3
0
mirror of https://github.com/jlu5/PyLink.git synced 2025-01-12 05:02:33 +01:00

Merge branch 'devel' into wip/unrealircd

This commit is contained in:
James Lu 2015-09-10 18:27:46 -07:00
commit da1b101bc4
26 changed files with 2200 additions and 1522 deletions

View File

@ -69,7 +69,8 @@ class Irc():
self.serverdata = conf['servers'][netname] self.serverdata = conf['servers'][netname]
self.sid = self.serverdata["sid"] self.sid = self.serverdata["sid"]
self.botdata = conf['bot'] self.botdata = conf['bot']
self.proto = proto self.protoname = proto.__name__
self.proto = proto.Class(self)
self.pingfreq = self.serverdata.get('pingfreq') or 30 self.pingfreq = self.serverdata.get('pingfreq') or 30
self.pingtimeout = self.pingfreq * 2 self.pingtimeout = self.pingfreq * 2
@ -146,7 +147,7 @@ class Irc():
sha1fp) sha1fp)
if checks_ok: if checks_ok:
self.proto.connect(self) self.proto.connect()
self.spawnMain() self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name) log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing() self.schedulePing()
@ -169,6 +170,15 @@ class Irc():
else: else:
return return
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)
else:
self.proto.messageClient(source, target, text)
def _disconnect(self): def _disconnect(self):
log.debug('(%s) Canceling pingTimer at %s due to _disconnect() call', self.name, time.time()) log.debug('(%s) Canceling pingTimer at %s due to _disconnect() call', self.name, time.time())
self.connected.clear() self.connected.clear()
@ -204,7 +214,7 @@ class Irc():
log.debug("(%s) <- %s", self.name, line) log.debug("(%s) <- %s", self.name, line)
hook_args = None hook_args = None
try: try:
hook_args = self.proto.handle_events(self, line) hook_args = self.proto.handle_events(line)
except Exception: except Exception:
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
return return
@ -253,7 +263,7 @@ class Irc():
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data) log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
def schedulePing(self): def schedulePing(self):
self.proto.pingServer(self) self.proto.pingServer()
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing) self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
self.pingTimer.daemon = True self.pingTimer.daemon = True
self.pingTimer.start() self.pingTimer.start()
@ -265,9 +275,9 @@ class Irc():
host = self.serverdata["hostname"] host = self.serverdata["hostname"]
log.info('(%s) Connected! Spawning main client %s.', self.name, nick) log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
olduserobj = self.pseudoclient olduserobj = self.pseudoclient
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)}) self.pseudoclient = self.proto.spawnClient(nick, ident, host, modes={("+o", None)})
for chan in self.serverdata['channels']: for chan in self.serverdata['channels']:
self.proto.joinClient(self, self.pseudoclient.uid, chan) self.proto.joinClient(self.pseudoclient.uid, chan)
# PyLink internal hook called when spawnMain is called and the # PyLink internal hook called when spawnMain is called and the
# contents of Irc().pseudoclient change. # contents of Irc().pseudoclient change.
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}]) self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
@ -343,7 +353,7 @@ class FakeIRC(Irc):
def run(self, data): def run(self, data):
"""Queues a message to the fake IRC server.""" """Queues a message to the fake IRC server."""
log.debug('<- ' + data) log.debug('<- ' + data)
hook_args = self.proto.handle_events(self, data) hook_args = self.proto.handle_events(data)
if hook_args is not None: if hook_args is not None:
self.hookmsgs.append(hook_args) self.hookmsgs.append(hook_args)
self.callHooks(hook_args) self.callHooks(hook_args)
@ -378,29 +388,29 @@ class FakeIRC(Irc):
self.hookmsgs = [] self.hookmsgs = []
return hookmsgs return hookmsgs
class FakeProto(): class Protocol():
"""Dummy protocol module for testing purposes.""" # TODO: Future state-keeping things will go here
def __init__(self): def __init__(self, irc):
self.hook_map = {} self.irc = irc
self.casemapping = 'rfc1459' self.casemapping = 'rfc1459'
self.__name__ = 'FakeProto' self.hook_map = {}
@staticmethod class FakeProto(Protocol):
def handle_events(irc, data): """Dummy protocol module for testing purposes."""
def handle_events(self, data):
pass pass
@staticmethod def connect(self):
def connect(irc):
pass pass
@staticmethod def spawnClient(self, nick, *args, **kwargs):
def spawnClient(irc, nick, *args, **kwargs):
uid = randint(1, 10000000000) uid = randint(1, 10000000000)
ts = int(time.time()) ts = int(time.time())
irc.users[uid] = user = IrcUser(nick, ts, uid) self.irc.users[uid] = user = IrcUser(nick, ts, uid)
return user return user
@staticmethod def joinClient(self, client, channel):
def joinClient(irc, client, channel): self.irc.channels[channel].users.add(client)
irc.channels[channel].users.add(client) self.irc.users[client].channels.add(channel)
irc.users[client].channels.add(channel)
FakeProto.Class = FakeProto

View File

@ -15,7 +15,7 @@ def handle_kick(irc, source, command, args):
kicked = args['target'] kicked = args['target']
channel = args['channel'] channel = args['channel']
if kicked == irc.pseudoclient.uid: if kicked == irc.pseudoclient.uid:
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) irc.proto.joinClient(irc.pseudoclient.uid, channel)
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
# Handle commands sent to the PyLink client (PRIVMSG) # Handle commands sent to the PyLink client (PRIVMSG)
@ -26,17 +26,17 @@ def handle_commands(irc, source, command, args):
cmd = cmd_args[0].lower() cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:] cmd_args = cmd_args[1:]
if cmd not in world.bot_commands: if cmd not in world.bot_commands:
utils.msg(irc, source, 'Error: Unknown command %r.' % cmd) irc.msg(source, 'Error: Unknown command %r.' % cmd)
return return
log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source)) log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source))
for func in world.bot_commands[cmd]: for func in world.bot_commands[cmd]:
try: try:
func(irc, source, cmd_args) func(irc, source, cmd_args)
except utils.NotAuthenticatedError: except utils.NotAuthenticatedError:
utils.msg(irc, source, 'Error: You are not authorized to perform this operation.') irc.msg(source, 'Error: You are not authorized to perform this operation.')
except Exception as e: except Exception as e:
log.exception('Unhandled exception caught in command %r', cmd) log.exception('Unhandled exception caught in command %r', cmd)
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e))) irc.msg(source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd). # Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).
@ -51,7 +51,7 @@ def handle_whois(irc, source, command, args):
sourceisOper = ('o', None) in irc.users[source].modes sourceisOper = ('o', None) in irc.users[source].modes
# https://www.alien.net.au/irc/irc2numerics.html # https://www.alien.net.au/irc/irc2numerics.html
# 311: sends nick!user@host information # 311: sends nick!user@host information
f(irc, server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname)) f(server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname))
# 319: RPL_WHOISCHANNELS, shows channel list # 319: RPL_WHOISCHANNELS, shows channel list
public_chans = [] public_chans = []
for chan in user.channels: for chan in user.channels:
@ -69,9 +69,9 @@ def handle_whois(irc, source, command, args):
chan = prefixchar + chan chan = prefixchar + chan
public_chans.append(chan) public_chans.append(chan)
if public_chans: if public_chans:
f(irc, server, 319, source, '%s :%s' % (nick, ' '.join(public_chans))) f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
# 312: sends the server the target is on, and its server description. # 312: sends the server the target is on, and its server description.
f(irc, server, 312, source, "%s %s :%s" % (nick, irc.serverdata['hostname'], f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name,
irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']))
# 313: sends a string denoting the target's operator privilege, # 313: sends a string denoting the target's operator privilege,
# only if they have umode +o. # only if they have umode +o.
@ -82,15 +82,15 @@ def handle_whois(irc, source, command, args):
opertype = "IRC Operator" opertype = "IRC Operator"
# Let's be gramatically correct. # Let's be gramatically correct.
n = 'n' if opertype[0].lower() in 'aeiou' else '' n = 'n' if opertype[0].lower() in 'aeiou' else ''
f(irc, server, 313, source, "%s :is a%s %s" % (nick, n, opertype)) f(server, 313, source, "%s :is a%s %s" % (nick, n, opertype))
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd. # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only show this to opers! # Only show this to opers!
if sourceisOper: if sourceisOper:
f(irc, server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes))) f(server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes)))
# 317: shows idle and signon time. However, we don't track the user's real # 317: shows idle and signon time. However, we don't track the user's real
# idle time, so we simply return 0. # idle time, so we simply return 0.
# <- 317 GL GL 15 1437632859 :seconds idle, signon time # <- 317 GL GL 15 1437632859 :seconds idle, signon time
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts)) f(server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
for func in world.whois_handlers: for func in world.whois_handlers:
# Iterate over custom plugin WHOIS handlers. They return a tuple # Iterate over custom plugin WHOIS handlers. They return a tuple
# or list with two arguments: the numeric, and the text to send. # or list with two arguments: the numeric, and the text to send.
@ -98,11 +98,11 @@ def handle_whois(irc, source, command, args):
res = func(irc, target) res = func(irc, target)
if res: if res:
num, text = res num, text = res
f(irc, server, num, source, text) f(server, num, source, text)
except Exception as e: except Exception as e:
# Again, we wouldn't want this to crash our service, in case # Again, we wouldn't want this to crash our service, in case
# something goes wrong! # something goes wrong!
log.exception('(%s) Error caught in WHOIS handler: %s', irc.name, e) log.exception('(%s) Error caught in WHOIS handler: %s', irc.name, e)
# 318: End of WHOIS. # 318: End of WHOIS.
f(irc, server, 318, source, "%s :End of /WHOIS list" % nick) f(server, 318, source, "%s :End of /WHOIS list" % nick)
utils.add_hook(handle_whois, 'WHOIS') utils.add_hook(handle_whois, 'WHOIS')

23
docs/technical/README.md Normal file
View File

@ -0,0 +1,23 @@
## PyLink Developer Documentation
Please note that as PyLink is still in its development phase, the API is subject to change.
Any documentation here is provided for reference only.
It is also really incomplete (contributors welcome!)
### Introduction
PyLink is an a modular, plugin-based IRC PseudoService framework. It uses swappable protocol modules and a hook-based system for calling plugins, allowing them to function regardless of the IRCd used.
<img src="core-structure.png" width="50%" height="50%">
### Contents
- [Writing plugins for PyLink](writing-plugins.md)
- [PyLink protocol module specification](pmodule-spec.md)
#### Future topics (not yet available)
- [PyLink hooks reference](hooks-reference.md)
- [Writing tests for PyLink modules](writing-tests.md)
- [Using PyLink's utils module](using-utils.md)

View File

@ -0,0 +1,27 @@
digraph G {
ratio = 1.3;
subgraph cluster_core {
label="PyLink Application Structure";
style="filled";
node [style="filled",color="white"];
color="lightblue";
subgraph cluster_testsuite {
label="Test Suite API";
style="filled";
node [style="filled",color="white"];
color=moccasin;
"Dummy protocol\nmodule" -> "Dummy IRC\nobject (FakeIRC)" [color=darkgreen];
"Dummy IRC\nobject (FakeIRC)" -> "Dummy protocol\nmodule" [color=darkgreen];
}
"IRC object" -> "Protocol module" -> "PyLink hooks" -> Plugins;
"Main program" -> "IRC object" [color=indigo] [label="Spawns 1/net"] [fontcolor=indigo];
"Main program" -> "Dummy IRC\nobject (FakeIRC)" [color=darkgreen] [label="(test suite runner)"] [fontcolor=darkgreen];
}
"Protocol module" -> "Remote IRCd" -> "Protocol module";
Plugins -> "Protocol module" [label="Communicates \nvia*Client/*Server\nfunctions"] [color=navyblue] [fontcolor=navyblue];
Plugins -> "Main program" [label="Registers commands\n& hook handlers"] [color=brown] [fontcolor=brown];
"Dummy protocol\nmodule" -> "PyLink hooks" [color=darkgreen];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,397 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><title>Python: module inspircd</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>inspircd</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/protocols/inspircd.py">/home/gl/pylink/protocols/inspircd.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="hashlib.html">hashlib</a><br>
<a href="os.html">os</a><br>
<a href="re.html">re</a><br>
</td><td width="25%" valign=top><a href="socket.html">socket</a><br>
<a href="ssl.html">ssl</a><br>
<a href="sys.html">sys</a><br>
</td><td width="25%" valign=top><a href="threading.html">threading</a><br>
<a href="time.html">time</a><br>
<a href="utils.html">utils</a><br>
</td><td width="25%" valign=top><a href="world.html">world</a><br>
</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="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a>(<a href="classes.html#Protocol">classes.Protocol</a>)
</font></dt><dd>
<dl>
<dt><font face="helvetica, arial"><a href="inspircd.html#InspIRCdProtocol">InspIRCdProtocol</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"><strong>Class</strong> = <a name="Class">class InspIRCdProtocol</a>(<a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a>)</font></td></tr>
<tr><td bgcolor="#ffc8d8"><tt>&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl><dt>Method resolution order:</dt>
<dd><a href="inspircd.html#InspIRCdProtocol">InspIRCdProtocol</a></dd>
<dd><a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a></dd>
<dd><a href="classes.html#Protocol">classes.Protocol</a></dd>
<dd><a href="builtins.html#object">builtins.object</a></dd>
</dl>
<hr>
Methods defined here:<br>
<dl><dt><a name="InspIRCdProtocol-__init__"><strong>__init__</strong></a>(self, irc)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-awayClient"><strong>awayClient</strong></a>(self, source, text)</dt><dd><tt>Sends&nbsp;an&nbsp;AWAY&nbsp;message&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;text&gt;&nbsp;can&nbsp;be&nbsp;an&nbsp;empty&nbsp;string<br>
to&nbsp;unset&nbsp;AWAY&nbsp;status.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-connect"><strong>connect</strong></a>(self)</dt><dd><tt>Initializes&nbsp;a&nbsp;connection&nbsp;to&nbsp;a&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_away"><strong>handle_away</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;AWAY&nbsp;messages.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_encap"><strong>handle_encap</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;encapsulated&nbsp;commands&nbsp;(ENCAP).&nbsp;Hook&nbsp;arguments<br>
returned&nbsp;by&nbsp;this&nbsp;should&nbsp;have&nbsp;a&nbsp;parse_as&nbsp;field,&nbsp;that&nbsp;sets&nbsp;the&nbsp;correct<br>
hook&nbsp;name&nbsp;for&nbsp;the&nbsp;message.<br>
&nbsp;<br>
For&nbsp;InspIRCd,&nbsp;the&nbsp;only&nbsp;ENCAP&nbsp;command&nbsp;we&nbsp;handle&nbsp;right&nbsp;now&nbsp;is&nbsp;KNOCK.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_fident"><strong>handle_fident</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FIDENT,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;ident&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fjoin"><strong>handle_fjoin</strong></a>(self, servernumeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;FJOIN&nbsp;commands&nbsp;(InspIRCd&nbsp;equivalent&nbsp;of&nbsp;JOIN/SJOIN).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fmode"><strong>handle_fmode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;the&nbsp;FMODE&nbsp;command,&nbsp;used&nbsp;for&nbsp;channel&nbsp;mode&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fname"><strong>handle_fname</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FNAME,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;real&nbsp;name/gecos&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_ftopic"><strong>handle_ftopic</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;FTOPIC&nbsp;(sets&nbsp;topic&nbsp;on&nbsp;burst).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_idle"><strong>handle_idle</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;the&nbsp;IDLE&nbsp;command,&nbsp;sent&nbsp;between&nbsp;servers&nbsp;in&nbsp;remote&nbsp;WHOIS&nbsp;queries.</tt></dd></dl>
<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_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>
opertype&nbsp;of&nbsp;the&nbsp;client,&nbsp;and&nbsp;assumes&nbsp;setting&nbsp;user&nbsp;mode&nbsp;+o&nbsp;on&nbsp;the&nbsp;caller.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_uid"><strong>handle_uid</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;UID&nbsp;commands&nbsp;(user&nbsp;introduction).</tt></dd></dl>
<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-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>
<dl><dt><a name="InspIRCdProtocol-killServer"><strong>killServer</strong></a>(self, numeric, target, reason)</dt><dd><tt>Sends&nbsp;a&nbsp;kill&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<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>
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>
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-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>
<dl><dt><a name="InspIRCdProtocol-sjoinServer"><strong>sjoinServer</strong></a>(self, server, channel, users, ts=None)</dt><dd><tt>Sends&nbsp;an&nbsp;SJOIN&nbsp;for&nbsp;a&nbsp;group&nbsp;of&nbsp;users&nbsp;to&nbsp;a&nbsp;channel.<br>
&nbsp;<br>
The&nbsp;sender&nbsp;should&nbsp;always&nbsp;be&nbsp;a&nbsp;Server&nbsp;ID&nbsp;(SID).&nbsp;TS&nbsp;is&nbsp;optional,&nbsp;and&nbsp;defaults<br>
to&nbsp;the&nbsp;one&nbsp;we've&nbsp;stored&nbsp;in&nbsp;the&nbsp;channel&nbsp;state&nbsp;if&nbsp;not&nbsp;given.<br>
&lt;users&gt;&nbsp;is&nbsp;a&nbsp;list&nbsp;of&nbsp;(prefix&nbsp;mode,&nbsp;UID)&nbsp;pairs:<br>
&nbsp;<br>
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>
&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-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-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>
<hr>
Methods inherited from <a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a>:<br>
<dl><dt><a name="InspIRCdProtocol-handle_error"><strong>handle_error</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;ERROR&nbsp;messages&nbsp;-&nbsp;these&nbsp;mean&nbsp;that&nbsp;our&nbsp;uplink&nbsp;has&nbsp;disconnected&nbsp;us!</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_kick"><strong>handle_kick</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;KICKs.</tt></dd></dl>
<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>
<dl><dt><a name="InspIRCdProtocol-handle_part"><strong>handle_part</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PART&nbsp;commands.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_squit"><strong>handle_squit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SQUITs&nbsp;(netsplits).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_topic"><strong>handle_topic</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;TOPIC&nbsp;changes&nbsp;from&nbsp;clients.&nbsp;For&nbsp;topic&nbsp;bursts,<br>
TB&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FTOPIC&nbsp;(InspIRCd)&nbsp;are&nbsp;used&nbsp;instead.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-kickClient"><strong>kickClient</strong></a>(self, numeric, channel, target, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;kick&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-kickServer"><strong>kickServer</strong></a>(self, numeric, channel, target, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;kick&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-messageClient"><strong>messageClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;PRIVMSG&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-nickClient"><strong>nickClient</strong></a>(self, numeric, newnick)</dt><dd><tt>Changes&nbsp;the&nbsp;nick&nbsp;of&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<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>
<dl><dt><a name="InspIRCdProtocol-partClient"><strong>partClient</strong></a>(self, client, channel, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;part&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<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>
Data descriptors inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<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="InspIRCdProtocol">class <strong>InspIRCdProtocol</strong></a>(<a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a>)</font></td></tr>
<tr><td bgcolor="#ffc8d8"><tt>&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl><dt>Method resolution order:</dt>
<dd><a href="inspircd.html#InspIRCdProtocol">InspIRCdProtocol</a></dd>
<dd><a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a></dd>
<dd><a href="classes.html#Protocol">classes.Protocol</a></dd>
<dd><a href="builtins.html#object">builtins.object</a></dd>
</dl>
<hr>
Methods defined here:<br>
<dl><dt><a name="InspIRCdProtocol-__init__"><strong>__init__</strong></a>(self, irc)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-awayClient"><strong>awayClient</strong></a>(self, source, text)</dt><dd><tt>Sends&nbsp;an&nbsp;AWAY&nbsp;message&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;text&gt;&nbsp;can&nbsp;be&nbsp;an&nbsp;empty&nbsp;string<br>
to&nbsp;unset&nbsp;AWAY&nbsp;status.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-connect"><strong>connect</strong></a>(self)</dt><dd><tt>Initializes&nbsp;a&nbsp;connection&nbsp;to&nbsp;a&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_away"><strong>handle_away</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;AWAY&nbsp;messages.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_encap"><strong>handle_encap</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;encapsulated&nbsp;commands&nbsp;(ENCAP).&nbsp;Hook&nbsp;arguments<br>
returned&nbsp;by&nbsp;this&nbsp;should&nbsp;have&nbsp;a&nbsp;parse_as&nbsp;field,&nbsp;that&nbsp;sets&nbsp;the&nbsp;correct<br>
hook&nbsp;name&nbsp;for&nbsp;the&nbsp;message.<br>
&nbsp;<br>
For&nbsp;InspIRCd,&nbsp;the&nbsp;only&nbsp;ENCAP&nbsp;command&nbsp;we&nbsp;handle&nbsp;right&nbsp;now&nbsp;is&nbsp;KNOCK.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_fident"><strong>handle_fident</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FIDENT,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;ident&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fjoin"><strong>handle_fjoin</strong></a>(self, servernumeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;FJOIN&nbsp;commands&nbsp;(InspIRCd&nbsp;equivalent&nbsp;of&nbsp;JOIN/SJOIN).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fmode"><strong>handle_fmode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;the&nbsp;FMODE&nbsp;command,&nbsp;used&nbsp;for&nbsp;channel&nbsp;mode&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fname"><strong>handle_fname</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FNAME,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;real&nbsp;name/gecos&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_ftopic"><strong>handle_ftopic</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;FTOPIC&nbsp;(sets&nbsp;topic&nbsp;on&nbsp;burst).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_idle"><strong>handle_idle</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;the&nbsp;IDLE&nbsp;command,&nbsp;sent&nbsp;between&nbsp;servers&nbsp;in&nbsp;remote&nbsp;WHOIS&nbsp;queries.</tt></dd></dl>
<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_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>
opertype&nbsp;of&nbsp;the&nbsp;client,&nbsp;and&nbsp;assumes&nbsp;setting&nbsp;user&nbsp;mode&nbsp;+o&nbsp;on&nbsp;the&nbsp;caller.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_uid"><strong>handle_uid</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;UID&nbsp;commands&nbsp;(user&nbsp;introduction).</tt></dd></dl>
<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-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>
<dl><dt><a name="InspIRCdProtocol-killServer"><strong>killServer</strong></a>(self, numeric, target, reason)</dt><dd><tt>Sends&nbsp;a&nbsp;kill&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<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>
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>
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-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>
<dl><dt><a name="InspIRCdProtocol-sjoinServer"><strong>sjoinServer</strong></a>(self, server, channel, users, ts=None)</dt><dd><tt>Sends&nbsp;an&nbsp;SJOIN&nbsp;for&nbsp;a&nbsp;group&nbsp;of&nbsp;users&nbsp;to&nbsp;a&nbsp;channel.<br>
&nbsp;<br>
The&nbsp;sender&nbsp;should&nbsp;always&nbsp;be&nbsp;a&nbsp;Server&nbsp;ID&nbsp;(SID).&nbsp;TS&nbsp;is&nbsp;optional,&nbsp;and&nbsp;defaults<br>
to&nbsp;the&nbsp;one&nbsp;we've&nbsp;stored&nbsp;in&nbsp;the&nbsp;channel&nbsp;state&nbsp;if&nbsp;not&nbsp;given.<br>
&lt;users&gt;&nbsp;is&nbsp;a&nbsp;list&nbsp;of&nbsp;(prefix&nbsp;mode,&nbsp;UID)&nbsp;pairs:<br>
&nbsp;<br>
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>
&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-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-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>
<hr>
Methods inherited from <a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6BaseProtocol</a>:<br>
<dl><dt><a name="InspIRCdProtocol-handle_error"><strong>handle_error</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;ERROR&nbsp;messages&nbsp;-&nbsp;these&nbsp;mean&nbsp;that&nbsp;our&nbsp;uplink&nbsp;has&nbsp;disconnected&nbsp;us!</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_kick"><strong>handle_kick</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;KICKs.</tt></dd></dl>
<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>
<dl><dt><a name="InspIRCdProtocol-handle_part"><strong>handle_part</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PART&nbsp;commands.</tt></dd></dl>
<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_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>
<dl><dt><a name="InspIRCdProtocol-handle_squit"><strong>handle_squit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SQUITs&nbsp;(netsplits).</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_topic"><strong>handle_topic</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;TOPIC&nbsp;changes&nbsp;from&nbsp;clients.&nbsp;For&nbsp;topic&nbsp;bursts,<br>
TB&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FTOPIC&nbsp;(InspIRCd)&nbsp;are&nbsp;used&nbsp;instead.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-kickClient"><strong>kickClient</strong></a>(self, numeric, channel, target, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;kick&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-kickServer"><strong>kickServer</strong></a>(self, numeric, channel, target, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;kick&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-messageClient"><strong>messageClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;PRIVMSG&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-nickClient"><strong>nickClient</strong></a>(self, numeric, newnick)</dt><dd><tt>Changes&nbsp;the&nbsp;nick&nbsp;of&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<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>
<dl><dt><a name="InspIRCdProtocol-partClient"><strong>partClient</strong></a>(self, client, channel, reason=None)</dt><dd><tt>Sends&nbsp;a&nbsp;part&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<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>
Data descriptors inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<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="#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>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>
<strong>log</strong> = &lt;logging.RootLogger object&gt;</td></tr></table>
</body></html>

View File

@ -0,0 +1,58 @@
# plugin_example.py: An example PyLink plugin.
# These two lines add PyLink's root directory to the PATH, so that importing things like
# 'utils' and 'log' work.
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
import random
# Example PRIVMSG hook that returns "hi there!" when PyLink's nick is mentioned
# in a channel.
# irc: The IRC object where the hook was called.
# source: The UID/numeric of the sender.
# command: The true command name where the hook originates. This may or may not be
# the same as the name of the hook, depending on context.
# args: The hook data (a dict) associated with the command. The available data
# keys differ by hook name (see the hooks reference for a list of which can
# be used).
def hook_privmsg(irc, source, command, args):
channel = args['target']
text = args['text']
# irc.pseudoclient stores the IrcUser object of the main PyLink client.
# (i.e. the user defined in the bot: section of the config)
if utils.isChannel(channel) and irc.pseudoclient.nick in text:
irc.msg(channel, 'hi there!')
log.info('%s said my name on channel %s (PRIVMSG hook caught)' % (source, channel))
utils.add_hook(hook_privmsg, 'PRIVMSG')
# Example command function. @utils.add_cmd binds it to an IRC command of the same name,
# but you can also use a different name by specifying a second 'name' argument (see below).
@utils.add_cmd
# irc: The IRC object where the command was called.
# source: The UID/numeric of the calling user.
# args: A list of command args (excluding the command name) that the command was called with.
def randint(irc, source, args):
# The docstring here is used as command help by the 'help' command, and formatted using the
# same line breaks as the raw string. You shouldn't make this text or any one line too long,
# to prevent flooding users or getting long lines cut off.
"""[<min>] [<max>]
Returns a random number between <min> and <max>. <min> and <max> default
to 1 and 10 respectively, if both aren't given."""
try:
rmin = args[0]
rmax = args[1]
except IndexError:
rmin, rmax = 1, 10
n = random.randint(rmin, rmax)
irc.msg(source, str(n))
# You can also bind a command function multiple times, and/or to different command names via a
# second argument.
# Note: no checking is done at the moment to prevent multiple plugins from binding to the same
# command name. The older command just gets replaced by the new one!
utils.add_cmd(randint, "random")

View File

@ -0,0 +1,27 @@
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:
- `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`

View File

@ -0,0 +1,70 @@
# Writing plugins for PyLink
PyLink plugins are modules that extend its functionality by giving it something to do. Without any plugins loaded, PyLink can only sit on a server and do absolutely nothing.
This guide, along with the sample plugin [`plugin_example.py`](plugin_example.py), aim to show the basics of writing plugins for PyLink.
### Receiving data from IRC
Plugins have three main ways of communicating with IRC: hooks, WHOIS handlers, and commands sent in PM to the main PyLink client. A simple plugin can use one, or any mixture of these.
### Hooks
Hooks are probably the most versatile form of communication. Each hook payload is formatted as a Python `dict`, with different data keys depending on the command.
For example, a `PRIVMSG` payload would give you the fields `target` and `text`, while a `PART` payload would only give you `channels` and `reason` fields.
There are many hook types available (one for each supported IRC command), and you can read more about them in the [PyLink hooks reference](hooks-reference.md).
Plugins can bind to hooks using the `utils.add_hook()` function like so: `utils.add_hook(function_name, 'PRIVMSG')`, where `function_name` is your function definition, and `PRIVMSG` is whatever hook name you want to bind to. Once set up, `function_name` will be called whenever the protocol module receives a `PRIVMSG` command.
Each hook-bound function takes 4 arguments: `irc, source, command, args`.
- **irc**: The IRC object where the hook was called. Plugins are globally loaded, so there will be one of these per network.
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
- **command**: The true command name where the hook originates. This may or may not be the same as the name of the hook, depending on context.
- **args**: The hook data (a `dict`) associated with the command. Again, the available data keys differ by hook name
(see the [hooks reference](hooks-reference.md) for a list of which can be used).
Hook functions do not return anything, and can raise exceptions to be caught by the core.
### PyLink commands
For plugins that interact with IRC users, there is also the option of binding to PM commands.
Commands are bound to using the `utils.add_cmd()` function: `utils.add_cmd(testcommand, "hello")`. Here, `testcommand` is the name of your function, and `hello` is the (optional) name of the command to bind to; if it is not specified, it'll use the same name as the function.
Now, your command function will be called whenever someone PMs the PyLink client with the command (e.g. `/msg PyLink hello`, case-insensitive).
Each command function takes 3 arguments: `irc, source, args`.
- **irc**: The IRC object where the command was called.
- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server).
- **args**: A `list` of space-separated command args (excluding the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']`
Command handlers do not return anything and can raise exceptions, which are caught by the core and automatically return an error message.
### WHOIS handlers
The third option, `WHOIS` handlers, are a lot more limited compared to the other options. They are solely used for `WHOIS` replies, **and only work on IRCds where WHOIS commands are sent to remote servers!** This includes Charybdis and UnrealIRCd, but **not** InspIRCd, which handles all `WHOIS` requests locally (the only thing sent between servers is an IDLE time query).
WHOIS replies are special in that any plugins wishing to add lines to a WHOIS reply must do so after the regular WHOIS lines (handled by the core), but before a special "End of WHOIS" line. This means that the regular hooks mechanism, which are only called after core handling, doesn't work here.
An example of a plugin WHOIS handler is in the relay plugin. WHOIS handler functions are added to the `world.whois_handlers` list using a simple `append()`. They should return either nothing or a two-length list: the first item being the WHOIS numeric, and the second the raw whois text.
```
def relayWhoisHandler(irc, target):
user = irc.users[target]
orig = getLocalUser(irc, target)
if orig:
network, remoteuid = orig
remotenick = world.networkobjects[network].users[remoteuid].nick
return [320, "%s :is a remote user connected via PyLink Relay. Home "
"network: %s; Home nick: %s" % (user.nick, network,
remotenick)]
world.whois_handlers.append(relayWhoisHandler)
```
### Sending data to IRC
Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds.
These functions are usually called in this fashion: `irc.proto.abcdClient(arg1, arg2)`. For example, the command `irc.proto.joinClient('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`.
For sending messages (e.g. replies to commands), a simpler form of `irc.msg(targetUID, text, notice=False, source=None)` is also used. The sender UID can be set here with the `source` argument, and defaults to the main PyLink client.

View File

@ -20,9 +20,9 @@ def spawnclient(irc, source, args):
try: try:
nick, ident, host = args[:3] nick, ident, host = args[:3]
except ValueError: except ValueError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 3: nick, user, host.") irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.")
return return
irc.proto.spawnClient(irc, nick, ident, host) irc.proto.spawnClient(nick, ident, host)
@utils.add_cmd @utils.add_cmd
def quit(irc, source, args): def quit(irc, source, args):
@ -33,14 +33,14 @@ def quit(irc, source, args):
try: try:
nick = args[0] nick = args[0]
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).") irc.msg(source, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).")
return return
if irc.pseudoclient.uid == utils.nickToUid(irc, nick): if irc.pseudoclient.uid == utils.nickToUid(irc, nick):
utils.msg(irc, source, "Error: Cannot quit the main PyLink PseudoClient!") irc.msg(source, "Error: Cannot quit the main PyLink PseudoClient!")
return return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client Quit' quitmsg = ' '.join(args[1:]) or 'Client Quit'
irc.proto.quitClient(irc, u, quitmsg) irc.proto.quitClient(u, quitmsg)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
def joinclient(irc, source, args): def joinclient(irc, source, args):
@ -54,14 +54,14 @@ def joinclient(irc, source, args):
if not clist: if not clist:
raise IndexError raise IndexError
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.") irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
for channel in clist: for channel in clist:
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) irc.msg(source, "Error: Invalid channel name %r." % channel)
return return
irc.proto.joinClient(irc, u, channel) irc.proto.joinClient(u, channel)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': channel, 'users': [u], irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': channel, 'users': [u],
'modes': irc.channels[channel].modes, 'modes': irc.channels[channel].modes,
'parse_as': 'JOIN'}]) 'parse_as': 'JOIN'}])
@ -77,15 +77,15 @@ def nick(irc, source, args):
nick = args[0] nick = args[0]
newnick = args[1] newnick = args[1]
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, newnick.") irc.msg(source, "Error: Not enough arguments. Needs 2: nick, newnick.")
return return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
if newnick in ('0', u): if newnick in ('0', u):
newnick = u newnick = u
elif not utils.isNick(newnick): elif not utils.isNick(newnick):
utils.msg(irc, source, 'Error: Invalid nickname %r.' % newnick) irc.msg(source, 'Error: Invalid nickname %r.' % newnick)
return return
irc.proto.nickClient(irc, u, newnick) irc.proto.nickClient(u, newnick)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
@utils.add_cmd @utils.add_cmd
@ -99,14 +99,14 @@ def part(irc, source, args):
clist = args[1].split(',') clist = args[1].split(',')
reason = ' '.join(args[2:]) reason = ' '.join(args[2:])
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.") irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
for channel in clist: for channel in clist:
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) irc.msg(source, "Error: Invalid channel name %r." % channel)
return return
irc.proto.partClient(irc, u, channel, reason) irc.proto.partClient(u, channel, reason)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}]) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
@utils.add_cmd @utils.add_cmd
@ -121,17 +121,17 @@ def kick(irc, source, args):
target = args[2] target = args[2]
reason = ' '.join(args[3:]) reason = ' '.join(args[3:])
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).") irc.msg(source, "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
return return
u = utils.nickToUid(irc, nick) or nick u = utils.nickToUid(irc, nick) or nick
targetu = utils.nickToUid(irc, target) targetu = utils.nickToUid(irc, target)
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) irc.msg(source, "Error: Invalid channel name %r." % channel)
return return
if utils.isInternalServer(irc, u): if utils.isInternalServer(irc, u):
irc.proto.kickServer(irc, u, channel, targetu, reason) irc.proto.kickServer(u, channel, targetu, reason)
else: else:
irc.proto.kickClient(irc, u, channel, targetu, reason) irc.proto.kickClient(u, channel, targetu, reason)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}]) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}])
@utils.add_cmd @utils.add_cmd
@ -143,24 +143,24 @@ def mode(irc, source, args):
try: try:
modesource, target, modes = args[0], args[1], args[2:] modesource, target, modes = args[0], args[1], args[2:]
except IndexError: except IndexError:
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.') irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
return return
if not modes: if not modes:
utils.msg(irc, source, "Error: No modes given to set!") irc.msg(source, "Error: No modes given to set!")
return return
parsedmodes = utils.parseModes(irc, target, modes) parsedmodes = utils.parseModes(irc, target, modes)
targetuid = utils.nickToUid(irc, target) targetuid = utils.nickToUid(irc, target)
if targetuid: if targetuid:
target = targetuid target = targetuid
elif not utils.isChannel(target): elif not utils.isChannel(target):
utils.msg(irc, source, "Error: Invalid channel or nick %r." % target) irc.msg(source, "Error: Invalid channel or nick %r." % target)
return return
if utils.isInternalServer(irc, modesource): if utils.isInternalServer(irc, modesource):
irc.proto.modeServer(irc, modesource, target, parsedmodes) irc.proto.modeServer(modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
else: else:
sourceuid = utils.nickToUid(irc, modesource) sourceuid = utils.nickToUid(irc, modesource)
irc.proto.modeClient(irc, sourceuid, target, parsedmodes) irc.proto.modeClient(sourceuid, target, parsedmodes)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
@utils.add_cmd @utils.add_cmd
@ -172,21 +172,21 @@ def msg(irc, source, args):
try: try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:]) msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError: except IndexError:
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, text.') irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, text.')
return return
sourceuid = utils.nickToUid(irc, msgsource) sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid: if not sourceuid:
utils.msg(irc, source, 'Error: Unknown user %r.' % msgsource) irc.msg(source, 'Error: Unknown user %r.' % msgsource)
return return
if not utils.isChannel(target): if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target) real_target = utils.nickToUid(irc, target)
if real_target is None: if real_target is None:
utils.msg(irc, source, 'Error: Unknown user %r.' % target) irc.msg(source, 'Error: Unknown user %r.' % target)
return return
else: else:
real_target = target real_target = target
if not text: if not text:
utils.msg(irc, source, 'Error: No text given.') irc.msg(source, 'Error: No text given.')
return return
irc.proto.messageClient(irc, sourceuid, real_target, text) irc.proto.messageClient(sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])

View File

@ -16,10 +16,10 @@ def status(irc, source, args):
Returns your current PyLink login status.""" Returns your current PyLink login status."""
identified = irc.users[source].identified identified = irc.users[source].identified
if identified: if identified:
utils.msg(irc, source, 'You are identified as \x02%s\x02.' % identified) irc.msg(source, 'You are identified as \x02%s\x02.' % identified)
else: else:
utils.msg(irc, source, 'You are not identified as anyone.') irc.msg(source, 'You are not identified as anyone.')
utils.msg(irc, source, 'Operator access: \x02%s\x02' % bool(utils.isOper(irc, source))) irc.msg(source, 'Operator access: \x02%s\x02' % bool(utils.isOper(irc, source)))
@utils.add_cmd @utils.add_cmd
def identify(irc, source, args): def identify(irc, source, args):
@ -29,17 +29,17 @@ def identify(irc, source, args):
try: try:
username, password = args[0], args[1] username, password = args[0], args[1]
except IndexError: except IndexError:
utils.msg(irc, source, 'Error: Not enough arguments.') irc.msg(source, 'Error: Not enough arguments.')
return return
# Usernames are case-insensitive, passwords are NOT. # Usernames are case-insensitive, passwords are NOT.
if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']: if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']:
realuser = conf['login']['user'] realuser = conf['login']['user']
irc.users[source].identified = realuser irc.users[source].identified = realuser
utils.msg(irc, source, 'Successfully logged in as %s.' % realuser) irc.msg(source, 'Successfully logged in as %s.' % realuser)
log.info("(%s) Successful login to %r by %s.", log.info("(%s) Successful login to %r by %s.",
irc.name, username, utils.getHostmask(irc, source)) irc.name, username, utils.getHostmask(irc, source))
else: else:
utils.msg(irc, source, 'Error: Incorrect credentials.') irc.msg(source, 'Error: Incorrect credentials.')
u = irc.users[source] u = irc.users[source]
log.warning("(%s) Failed login to %r from %s.", log.warning("(%s) Failed login to %r from %s.",
irc.name, username, utils.getHostmask(irc, source)) irc.name, username, utils.getHostmask(irc, source))
@ -54,8 +54,8 @@ def listcommands(irc, source, args):
nfuncs = len(world.bot_commands[cmd]) nfuncs = len(world.bot_commands[cmd])
if nfuncs > 1: if nfuncs > 1:
cmds[idx] = '%s(x%s)' % (cmd, nfuncs) cmds[idx] = '%s(x%s)' % (cmd, nfuncs)
utils.msg(irc, source, 'Available commands include: %s' % ', '.join(cmds)) irc.msg(source, 'Available commands include: %s' % ', '.join(cmds))
utils.msg(irc, source, 'To see help on a specific command, type \x02help <command>\x02.') irc.msg(source, 'To see help on a specific command, type \x02help <command>\x02.')
utils.add_cmd(listcommands, 'list') utils.add_cmd(listcommands, 'list')
@utils.add_cmd @utils.add_cmd
@ -69,12 +69,12 @@ def help(irc, source, args):
listcommands(irc, source, args) listcommands(irc, source, args)
return return
if command not in world.bot_commands: if command not in world.bot_commands:
utils.msg(irc, source, 'Error: Unknown command %r.' % command) irc.msg(source, 'Error: Unknown command %r.' % command)
return return
else: else:
funcs = world.bot_commands[command] funcs = world.bot_commands[command]
if len(funcs) > 1: if len(funcs) > 1:
utils.msg(irc, source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s' irc.msg(source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
% (len(funcs), command, ', '.join([func.__module__ for func in funcs]))) % (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
for func in funcs: for func in funcs:
doc = func.__doc__ doc = func.__doc__
@ -85,9 +85,9 @@ def help(irc, source, args):
# arguments the command takes. # arguments the command takes.
lines[0] = '\x02%s %s\x02 (plugin: %r)' % (command, lines[0], mod) lines[0] = '\x02%s %s\x02 (plugin: %r)' % (command, lines[0], mod)
for line in lines: for line in lines:
utils.msg(irc, source, line.strip()) irc.msg(source, line.strip())
else: else:
utils.msg(irc, source, "Error: Command %r (from plugin %r) " irc.msg(source, "Error: Command %r (from plugin %r) "
"doesn't offer any help." % (command, mod)) "doesn't offer any help." % (command, mod))
return return
@ -99,17 +99,17 @@ def showuser(irc, source, args):
try: try:
target = args[0] target = args[0]
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: nick.") irc.msg(source, "Error: Not enough arguments. Needs 1: nick.")
return return
u = utils.nickToUid(irc, target) or target u = utils.nickToUid(irc, target) or target
# Only show private info if the person is calling 'showuser' on themselves, # Only show private info if the person is calling 'showuser' on themselves,
# or is an oper. # or is an oper.
verbose = utils.isOper(irc, source) or u == source verbose = utils.isOper(irc, source) or u == source
if u not in irc.users: if u not in irc.users:
utils.msg(irc, source, 'Error: Unknown user %r.' % target) irc.msg(source, 'Error: Unknown user %r.' % target)
return return
f = lambda s: utils.msg(irc, source, s) f = lambda s: irc.msg(source, s)
userobj = irc.users[u] userobj = irc.users[u]
f('Information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident, f('Information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
userobj.host, userobj.realname)) userobj.host, userobj.realname))

View File

@ -14,7 +14,7 @@ def _exec(irc, source, args):
utils.checkAuthenticated(irc, source, allowOper=False) utils.checkAuthenticated(irc, source, allowOper=False)
args = ' '.join(args) args = ' '.join(args)
if not args.strip(): if not args.strip():
utils.msg(irc, source, 'No code entered!') irc.msg(source, 'No code entered!')
return return
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source)) log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals()) exec(args, globals(), locals())

View File

@ -1,19 +0,0 @@
# hooks.py: test of PyLink hooks
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
def hook_join(irc, source, command, args):
channel = args['channel']
users = args['users']
log.info('%s joined channel %s (JOIN hook caught)' % (users, channel))
utils.add_hook(hook_join, 'JOIN')
def hook_privmsg(irc, source, command, args):
channel = args['target']
text = args['text']
if utils.isChannel(channel) and irc.pseudoclient.nick in text:
utils.msg(irc, channel, 'hi there!')
log.info('%s said my name on channel %s (PRIVMSG hook caught)' % (source, channel))
utils.add_hook(hook_privmsg, 'PRIVMSG')

View File

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

View File

@ -24,7 +24,7 @@ relayusers = defaultdict(dict)
spawnlocks = defaultdict(threading.Lock) spawnlocks = defaultdict(threading.Lock)
savecache = ExpiringDict(max_len=5, max_age_seconds=10) savecache = ExpiringDict(max_len=5, max_age_seconds=10)
def relayWhoisHandlers(irc, target): def relayWhoisHandler(irc, target):
user = irc.users[target] user = irc.users[target]
orig = getLocalUser(irc, target) orig = getLocalUser(irc, target)
if orig: if orig:
@ -33,13 +33,13 @@ def relayWhoisHandlers(irc, target):
return [320, "%s :is a remote user connected via PyLink Relay. Home " return [320, "%s :is a remote user connected via PyLink Relay. Home "
"network: %s; Home nick: %s" % (user.nick, network, "network: %s; Home nick: %s" % (user.nick, network,
remotenick)] remotenick)]
world.whois_handlers.append(relayWhoisHandlers) world.whois_handlers.append(relayWhoisHandler)
def normalizeNick(irc, netname, nick, separator=None, uid=''): def normalizeNick(irc, netname, nick, separator=None, uid=''):
separator = separator or irc.serverdata.get('separator') or "/" separator = separator or irc.serverdata.get('separator') or "/"
log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator)
orig_nick = nick orig_nick = nick
protoname = irc.proto.__name__ protoname = irc.protoname
maxnicklen = irc.maxnicklen maxnicklen = irc.maxnicklen
if '/' not in separator or not protoname.startswith(('insp', 'unreal')): if '/' not in separator or not protoname.startswith(('insp', 'unreal')):
# Charybdis doesn't allow / in usernames, and will SQUIT with # Charybdis doesn't allow / in usernames, and will SQUIT with
@ -109,9 +109,9 @@ def save(irc, source, args):
Saves the relay database to disk.""" Saves the relay database to disk."""
if utils.isOper(irc, source): if utils.isOper(irc, source):
exportDB() exportDB()
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: You are not authenticated!') irc.msg(source, 'Error: You are not authenticated!')
return return
def getPrefixModes(irc, remoteirc, channel, user): def getPrefixModes(irc, remoteirc, channel, user):
@ -172,7 +172,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
hideoper_mode = remoteirc.umodes.get('hideoper') hideoper_mode = remoteirc.umodes.get('hideoper')
if hideoper_mode: if hideoper_mode:
modes.append((hideoper_mode, None)) modes.append((hideoper_mode, None))
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, u = remoteirc.proto.spawnClient(nick, ident=ident,
host=host, realname=realname, host=host, realname=realname,
modes=modes, ts=userobj.ts, modes=modes, ts=userobj.ts,
opertype=opertype).uid opertype=opertype).uid
@ -180,7 +180,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
remoteirc.users[u].opertype = opertype remoteirc.users[u].opertype = opertype
away = userobj.away away = userobj.away
if away: if away:
remoteirc.proto.awayClient(remoteirc, u, away) remoteirc.proto.awayClient(u, away)
relayusers[(irc.name, user)][remoteirc.name] = u relayusers[(irc.name, user)][remoteirc.name] = u
return u return u
@ -275,11 +275,11 @@ def initializeChannel(irc, channel):
# Only update the topic if it's different from what we already have, # Only update the topic if it's different from what we already have,
# and topic bursting is complete. # and topic bursting is complete.
if remoteirc.channels[remotechan].topicset and topic != irc.channels[channel].topic: if remoteirc.channels[remotechan].topicset and topic != irc.channels[channel].topic:
irc.proto.topicServer(irc, irc.sid, channel, topic) irc.proto.topicServer(irc.sid, channel, topic)
# Send our users and channel modes to the other nets # Send our users and channel modes to the other nets
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users) log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
relayJoins(irc, channel, c.users, c.ts) relayJoins(irc, channel, c.users, c.ts)
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) irc.proto.joinClient(irc.pseudoclient.uid, channel)
def handle_join(irc, numeric, command, args): def handle_join(irc, numeric, command, args):
channel = args['channel'] channel = args['channel']
@ -294,7 +294,7 @@ utils.add_hook(handle_join, 'JOIN')
def handle_quit(irc, numeric, command, args): def handle_quit(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].copy().items(): for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = world.networkobjects[netname] remoteirc = world.networkobjects[netname]
remoteirc.proto.quitClient(remoteirc, user, args['text']) remoteirc.proto.quitClient(user, args['text'])
del relayusers[(irc.name, numeric)] del relayusers[(irc.name, numeric)]
utils.add_hook(handle_quit, 'QUIT') utils.add_hook(handle_quit, 'QUIT')
@ -310,7 +310,7 @@ def handle_nick(irc, numeric, command, args):
remoteirc = world.networkobjects[netname] remoteirc = world.networkobjects[netname]
newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user) newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
if remoteirc.users[user].nick != newnick: if remoteirc.users[user].nick != newnick:
remoteirc.proto.nickClient(remoteirc, user, newnick) remoteirc.proto.nickClient(user, newnick)
utils.add_hook(handle_nick, 'NICK') utils.add_hook(handle_nick, 'NICK')
def handle_part(irc, numeric, command, args): def handle_part(irc, numeric, command, args):
@ -325,9 +325,9 @@ def handle_part(irc, numeric, command, args):
remotechan = findRemoteChan(irc, remoteirc, channel) remotechan = findRemoteChan(irc, remoteirc, channel)
if remotechan is None: if remotechan is None:
continue continue
remoteirc.proto.partClient(remoteirc, user, remotechan, text) remoteirc.proto.partClient(user, remotechan, text)
if not remoteirc.users[user].channels: if not remoteirc.users[user].channels:
remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.') remoteirc.proto.quitClient(user, 'Left all shared channels.')
del relayusers[(irc.name, numeric)][remoteirc.name] del relayusers[(irc.name, numeric)][remoteirc.name]
utils.add_hook(handle_part, 'PART') utils.add_hook(handle_part, 'PART')
@ -352,7 +352,7 @@ def handle_privmsg(irc, numeric, command, args):
# it's the only way we can make sure they have a spawned client on ALL # it's the only way we can make sure they have a spawned client on ALL
# of the linked networks. This affects -n channels too; see # of the linked networks. This affects -n channels too; see
# https://github.com/GLolol/PyLink/issues/91 for an explanation of why. # https://github.com/GLolol/PyLink/issues/91 for an explanation of why.
utils.msg(irc, numeric, 'Error: You must be in %r in order to send ' irc.msg(numeric, 'Error: You must be in %r in order to send '
'messages over the relay.' % target, notice=True) 'messages over the relay.' % target, notice=True)
return return
if utils.isChannel(target): if utils.isChannel(target):
@ -363,9 +363,9 @@ def handle_privmsg(irc, numeric, command, args):
continue continue
real_target = prefix + real_target real_target = prefix + real_target
if notice: if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text) remoteirc.proto.noticeClient(user, real_target, text)
else: else:
remoteirc.proto.messageClient(remoteirc, user, real_target, text) remoteirc.proto.messageClient(user, real_target, text)
else: else:
remoteuser = getLocalUser(irc, target) remoteuser = getLocalUser(irc, target)
if remoteuser is None: if remoteuser is None:
@ -376,16 +376,16 @@ def handle_privmsg(irc, numeric, command, args):
# on the remote network, and we won't have anything to send our # on the remote network, and we won't have anything to send our
# messages from. # messages from.
if homenet not in remoteusers.keys(): if homenet not in remoteusers.keys():
utils.msg(irc, numeric, 'Error: You must be in a common channel ' irc.msg(numeric, 'Error: You must be in a common channel '
'with %r in order to send messages.' % \ 'with %r in order to send messages.' % \
irc.users[target].nick, notice=True) irc.users[target].nick, notice=True)
return return
remoteirc = world.networkobjects[homenet] remoteirc = world.networkobjects[homenet]
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if notice: if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text) remoteirc.proto.noticeClient(user, real_target, text)
else: else:
remoteirc.proto.messageClient(remoteirc, user, real_target, text) remoteirc.proto.messageClient(user, real_target, text)
utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'PRIVMSG')
utils.add_hook(handle_privmsg, 'NOTICE') utils.add_hook(handle_privmsg, 'NOTICE')
@ -431,12 +431,12 @@ def handle_kick(irc, source, command, args):
# kick ops, admins can't kick owners, etc. # kick ops, admins can't kick owners, etc.
modes = getPrefixModes(remoteirc, irc, remotechan, real_target) modes = getPrefixModes(remoteirc, irc, remotechan, real_target)
# Join the kicked client back with its respective modes. # Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)]) irc.proto.sjoinServer(irc.sid, channel, [(modes, target)])
if kicker in irc.users: if kicker in irc.users:
log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.', log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
irc.name, args['text'], irc.users[source].nick, irc.name, args['text'], irc.users[source].nick,
remoteirc.users[real_target].nick, remoteirc.name, channel) remoteirc.users[real_target].nick, remoteirc.name, channel)
utils.msg(irc, kicker, "This channel is claimed; your kick to " irc.msg(kicker, "This channel is claimed; your kick to "
"%s has been blocked because you are not " "%s has been blocked because you are not "
"(half)opped." % channel, notice=True) "(half)opped." % channel, notice=True)
else: else:
@ -450,7 +450,7 @@ def handle_kick(irc, source, command, args):
# Propogate the kick! # Propogate the kick!
if real_kicker: if real_kicker:
log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan,real_kicker, kicker, irc.name) log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan,real_kicker, kicker, irc.name)
remoteirc.proto.kickClient(remoteirc, real_kicker, remoteirc.proto.kickClient(real_kicker,
remotechan, real_target, text) remotechan, real_target, text)
else: else:
# Kick originated from a server, or the kicker isn't in any # Kick originated from a server, or the kicker isn't in any
@ -464,17 +464,17 @@ def handle_kick(irc, source, command, args):
text = "(%s/%s) %s" % (kname, irc.name, text) text = "(%s/%s) %s" % (kname, irc.name, text)
except AttributeError: except AttributeError:
text = "(<unknown kicker>@%s) %s" % (irc.name, text) text = "(<unknown kicker>@%s) %s" % (irc.name, text)
remoteirc.proto.kickServer(remoteirc, remoteirc.sid, remoteirc.proto.kickServer(remoteirc.sid,
remotechan, real_target, text) remotechan, real_target, text)
# If the target isn't on any channels, quit them. # If the target isn't on any channels, quit them.
if origuser and origuser[0] != remoteirc.name and not remoteirc.users[real_target].channels: if origuser and origuser[0] != remoteirc.name and not remoteirc.users[real_target].channels:
del relayusers[origuser][remoteirc.name] del relayusers[origuser][remoteirc.name]
remoteirc.proto.quitClient(remoteirc, real_target, 'Left all shared channels.') remoteirc.proto.quitClient(real_target, 'Left all shared channels.')
if origuser and not irc.users[target].channels: if origuser and not irc.users[target].channels:
del relayusers[origuser][irc.name] del relayusers[origuser][irc.name]
irc.proto.quitClient(irc, target, 'Left all shared channels.') irc.proto.quitClient(target, 'Left all shared channels.')
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
@ -493,7 +493,7 @@ def handle_chgclient(irc, source, command, args):
for netname, user in relayusers[(irc.name, target)].items(): for netname, user in relayusers[(irc.name, target)].items():
remoteirc = world.networkobjects[netname] remoteirc = world.networkobjects[netname]
try: try:
remoteirc.proto.updateClient(remoteirc, user, field, text) remoteirc.proto.updateClient(user, field, text)
except NotImplementedError: # IRCd doesn't support changing the field we want except NotImplementedError: # IRCd doesn't support changing the field we want
log.debug('(%s) Ignoring changing field %r of %s on %s (for %s/%s);' log.debug('(%s) Ignoring changing field %r of %s on %s (for %s/%s);'
' remote IRCd doesn\'t support it', irc.name, field, ' remote IRCd doesn\'t support it', irc.name, field,
@ -584,9 +584,9 @@ def relayModes(irc, remoteirc, sender, channel, modes=None):
# Check if the sender is a user; remember servers are allowed to set modes too. # Check if the sender is a user; remember servers are allowed to set modes too.
u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False) u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False)
if u: if u:
remoteirc.proto.modeClient(remoteirc, u, remotechan, supported_modes) remoteirc.proto.modeClient(u, remotechan, supported_modes)
else: else:
remoteirc.proto.modeServer(remoteirc, remoteirc.sid, remotechan, supported_modes) remoteirc.proto.modeServer(remoteirc.sid, remotechan, supported_modes)
def getSupportedUmodes(irc, remoteirc, modes): def getSupportedUmodes(irc, remoteirc, modes):
supported_modes = [] supported_modes = []
@ -613,7 +613,7 @@ def getSupportedUmodes(irc, remoteirc, modes):
log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because "
"the remote network (%s)'s IRCd (%s) doesn't support it.", "the remote network (%s)'s IRCd (%s) doesn't support it.",
irc.name, modechar, arg, remoteirc.name, irc.name, modechar, arg, remoteirc.name,
remoteirc.proto.__name__) remoteirc.protoname)
return supported_modes return supported_modes
def handle_mode(irc, numeric, command, args): def handle_mode(irc, numeric, command, args):
@ -636,7 +636,7 @@ def handle_mode(irc, numeric, command, args):
modes.append(('-%s' % hideoper_mode, None)) modes.append(('-%s' % hideoper_mode, None))
remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False) remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
if remoteuser and modes: if remoteuser and modes:
remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes) remoteirc.proto.modeClient(remoteuser, remoteuser, modes)
utils.add_hook(handle_mode, 'MODE') utils.add_hook(handle_mode, 'MODE')
@ -654,9 +654,9 @@ def handle_topic(irc, numeric, command, args):
# This might originate from a server too. # This might originate from a server too.
remoteuser = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) remoteuser = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if remoteuser: if remoteuser:
remoteirc.proto.topicClient(remoteirc, remoteuser, remotechan, topic) remoteirc.proto.topicClient(remoteuser, remotechan, topic)
else: else:
remoteirc.proto.topicServer(remoteirc, remoteirc.sid, remotechan, topic) remoteirc.proto.topicServer(remoteirc.sid, remotechan, topic)
utils.add_hook(handle_topic, 'TOPIC') utils.add_hook(handle_topic, 'TOPIC')
def handle_kill(irc, numeric, command, args): def handle_kill(irc, numeric, command, args):
@ -676,12 +676,12 @@ def handle_kill(irc, numeric, command, args):
modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) modes = getPrefixModes(remoteirc, irc, localchan, realuser[1])
log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser)
client = getRemoteUser(remoteirc, irc, realuser[1]) client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)]) irc.proto.sjoinServer(irc.sid, localchan, [(modes, client)])
if userdata and numeric in irc.users: if userdata and numeric in irc.users:
log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.', log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.',
irc.name, args['text'], irc.users[numeric].nick, irc.name, args['text'], irc.users[numeric].nick,
remoteirc.users[realuser[1]].nick, realuser[0]) remoteirc.users[realuser[1]].nick, realuser[0])
utils.msg(irc, numeric, "Your kill to %s has been blocked " irc.msg(numeric, "Your kill to %s has been blocked "
"because PyLink does not allow killing" "because PyLink does not allow killing"
" users over the relay at this time." % \ " users over the relay at this time." % \
userdata.nick, notice=True) userdata.nick, notice=True)
@ -745,10 +745,10 @@ def relayJoins(irc, channel, users, ts, burst=True):
# Burst was explicitly given, or we're trying to join multiple # Burst was explicitly given, or we're trying to join multiple
# users/someone with a prefix. # users/someone with a prefix.
if burst or len(queued_users) > 1 or queued_users[0][0]: if burst or len(queued_users) > 1 or queued_users[0][0]:
remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, queued_users, ts=ts) remoteirc.proto.sjoinServer(remoteirc.sid, remotechan, queued_users, ts=ts)
relayModes(irc, remoteirc, irc.sid, channel, irc.channels[channel].modes) relayModes(irc, remoteirc, irc.sid, channel, irc.channels[channel].modes)
else: else:
remoteirc.proto.joinClient(remoteirc, queued_users[0][1], remotechan) remoteirc.proto.joinClient(queued_users[0][1], remotechan)
def relayPart(irc, channel, user): def relayPart(irc, channel, user):
for name, remoteirc in world.networkobjects.items(): for name, remoteirc in world.networkobjects.items():
@ -762,16 +762,16 @@ def relayPart(irc, channel, user):
log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser) log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser)
if remotechan is None or remoteuser is None: if remotechan is None or remoteuser is None:
continue continue
remoteirc.proto.partClient(remoteirc, remoteuser, remotechan, 'Channel delinked.') remoteirc.proto.partClient(remoteuser, remotechan, 'Channel delinked.')
if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels: if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels:
remoteirc.proto.quitClient(remoteirc, remoteuser, 'Left all shared channels.') remoteirc.proto.quitClient(remoteuser, 'Left all shared channels.')
del relayusers[(irc.name, user)][remoteirc.name] del relayusers[(irc.name, user)][remoteirc.name]
def removeChannel(irc, channel): def removeChannel(irc, channel):
if irc is None: if irc is None:
return return
if channel not in map(str.lower, irc.serverdata['channels']): if channel not in map(str.lower, irc.serverdata['channels']):
irc.proto.partClient(irc, irc.pseudoclient.uid, channel, 'Channel delinked.') irc.proto.partClient(irc.pseudoclient.uid, channel, 'Channel delinked.')
relay = findRelay((irc.name, channel)) relay = findRelay((irc.name, channel))
if relay: if relay:
for user in irc.channels[channel].users.copy(): for user in irc.channels[channel].users.copy():
@ -782,12 +782,12 @@ def removeChannel(irc, channel):
if user == irc.pseudoclient.uid and channel in \ if user == irc.pseudoclient.uid and channel in \
irc.serverdata['channels']: irc.serverdata['channels']:
continue continue
irc.proto.partClient(irc, user, channel, 'Channel delinked.') irc.proto.partClient(user, channel, 'Channel delinked.')
# Don't ever quit it either... # Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels: if user != irc.pseudoclient.uid and not irc.users[user].channels:
remoteuser = getLocalUser(irc, user) remoteuser = getLocalUser(irc, user)
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]
irc.proto.quitClient(irc, user, 'Left all shared channels.') irc.proto.quitClient(user, 'Left all shared channels.')
@utils.add_cmd @utils.add_cmd
def create(irc, source, args): def create(irc, source, args):
@ -797,20 +797,20 @@ def create(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.") irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
return return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel) irc.msg(source, 'Error: Invalid channel %r.' % channel)
return return
if source not in irc.channels[channel].users: if source not in irc.channels[channel].users:
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % channel) irc.msg(source, 'Error: You must be in %r to complete this operation.' % channel)
return return
if not utils.isOper(irc, source): if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') irc.msg(source, 'Error: You must be opered in order to complete this operation.')
return return
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()} db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
initializeChannel(irc, channel) initializeChannel(irc, channel)
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
@utils.add_cmd @utils.add_cmd
def destroy(irc, source, args): def destroy(irc, source, args):
@ -820,13 +820,13 @@ def destroy(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.") irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
return return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel) irc.msg(source, 'Error: Invalid channel %r.' % channel)
return return
if not utils.isOper(irc, source): if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') irc.msg(source, 'Error: You must be opered in order to complete this operation.')
return return
entry = (irc.name, channel) entry = (irc.name, channel)
@ -835,9 +835,9 @@ def destroy(irc, source, args):
removeChannel(world.networkobjects.get(link[0]), link[1]) removeChannel(world.networkobjects.get(link[0]), link[1])
removeChannel(irc, channel) removeChannel(irc, channel)
del db[entry] del db[entry]
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel) irc.msg(source, 'Error: No such relay %r exists.' % channel)
return return
@utils.add_cmd @utils.add_cmd
@ -850,7 +850,7 @@ def link(irc, source, args):
channel = utils.toLower(irc, args[1]) channel = utils.toLower(irc, args[1])
remotenet = args[0].lower() remotenet = args[0].lower()
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).") irc.msg(source, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
return return
try: try:
localchan = utils.toLower(irc, args[2]) localchan = utils.toLower(irc, args[2])
@ -858,39 +858,39 @@ def link(irc, source, args):
localchan = channel localchan = channel
for c in (channel, localchan): for c in (channel, localchan):
if not utils.isChannel(c): if not utils.isChannel(c):
utils.msg(irc, source, 'Error: Invalid channel %r.' % c) irc.msg(source, 'Error: Invalid channel %r.' % c)
return return
if source not in irc.channels[localchan].users: if source not in irc.channels[localchan].users:
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % localchan) irc.msg(source, 'Error: You must be in %r to complete this operation.' % localchan)
return return
if not utils.isOper(irc, source): if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') irc.msg(source, 'Error: You must be opered in order to complete this operation.')
return return
if remotenet not in world.networkobjects: if remotenet not in world.networkobjects:
utils.msg(irc, source, 'Error: No network named %r exists.' % remotenet) irc.msg(source, 'Error: No network named %r exists.' % remotenet)
return return
localentry = findRelay((irc.name, localchan)) localentry = findRelay((irc.name, localchan))
if localentry: if localentry:
utils.msg(irc, source, 'Error: Channel %r is already part of a relay.' % localchan) irc.msg(source, 'Error: Channel %r is already part of a relay.' % localchan)
return return
try: try:
entry = db[(remotenet, channel)] entry = db[(remotenet, channel)]
except KeyError: except KeyError:
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel) irc.msg(source, 'Error: No such relay %r exists.' % channel)
return return
else: else:
if irc.name in entry['blocked_nets']: if irc.name in entry['blocked_nets']:
utils.msg(irc, source, 'Error: Access denied (network is banned from linking to this channel).') irc.msg(source, 'Error: Access denied (network is banned from linking to this channel).')
return return
for link in entry['links']: for link in entry['links']:
if link[0] == irc.name: if link[0] == irc.name:
utils.msg(irc, source, "Error: Remote channel '%s%s' is already" irc.msg(source, "Error: Remote channel '%s%s' is already"
" linked here as %r." % (remotenet, " linked here as %r." % (remotenet,
channel, link[1])) channel, link[1]))
return return
entry['links'].add((irc.name, localchan)) entry['links'].add((irc.name, localchan))
initializeChannel(irc, localchan) initializeChannel(irc, localchan)
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
@utils.add_cmd @utils.add_cmd
def delink(irc, source, args): def delink(irc, source, args):
@ -901,23 +901,23 @@ def delink(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).") irc.msg(source, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).")
return return
try: try:
remotenet = args[1].lower() remotenet = args[1].lower()
except IndexError: except IndexError:
remotenet = None remotenet = None
if not utils.isOper(irc, source): if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') irc.msg(source, 'Error: You must be opered in order to complete this operation.')
return return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel) irc.msg(source, 'Error: Invalid channel %r.' % channel)
return return
entry = findRelay((irc.name, channel)) entry = findRelay((irc.name, channel))
if entry: if entry:
if entry[0] == irc.name: # We own this channel. if entry[0] == irc.name: # We own this channel.
if not remotenet: if not remotenet:
utils.msg(irc, source, "Error: You must select a network to " irc.msg(source, "Error: You must select a network to "
"delink, or use the 'destroy' command to remove " "delink, or use the 'destroy' command to remove "
"this relay entirely (it was created on the current " "this relay entirely (it was created on the current "
"network).") "network).")
@ -930,9 +930,9 @@ def delink(irc, source, args):
else: else:
removeChannel(irc, channel) removeChannel(irc, channel)
db[entry]['links'].remove((irc.name, channel)) db[entry]['links'].remove((irc.name, channel))
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: No such relay %r.' % channel) irc.msg(source, 'Error: No such relay %r.' % channel)
def initializeAll(irc): def initializeAll(irc):
log.debug('(%s) initializeAll: waiting for world.started', irc.name) log.debug('(%s) initializeAll: waiting for world.started', irc.name)
@ -986,7 +986,7 @@ def handle_save(irc, numeric, command, args):
newnick = normalizeNick(irc, remotenet, nick) newnick = normalizeNick(irc, remotenet, nick)
log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s', log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s',
irc.name, target, nick, newnick) irc.name, target, nick, newnick)
irc.proto.nickClient(irc, target, newnick) irc.proto.nickClient(target, newnick)
else: else:
log.warning('(%s) SAVE received for relay client %r (%s), not ' log.warning('(%s) SAVE received for relay client %r (%s), not '
'fixing nick again due to 5 failed attempts in ' 'fixing nick again due to 5 failed attempts in '
@ -1007,7 +1007,7 @@ def linked(irc, source, args):
networks = list(world.networkobjects.keys()) networks = list(world.networkobjects.keys())
networks.remove(irc.name) networks.remove(irc.name)
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks)) s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks))
utils.msg(irc, source, s) irc.msg(source, s)
# Sort relay DB by channel name, and then sort. # Sort relay DB by channel name, and then sort.
for k, v in sorted(db.items(), key=lambda channel: channel[0][1]): for k, v in sorted(db.items(), key=lambda channel: channel[0][1]):
s = '\x02%s%s\x02 ' % k s = '\x02%s%s\x02 ' % k
@ -1015,12 +1015,12 @@ def linked(irc, source, args):
s += ' '.join([''.join(link) for link in v['links']]) s += ' '.join([''.join(link) for link in v['links']])
else: else:
s += '(no relays yet)' s += '(no relays yet)'
utils.msg(irc, source, s) irc.msg(source, s)
def handle_away(irc, numeric, command, args): def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = world.networkobjects[netname] remoteirc = world.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text']) remoteirc.proto.awayClient(user, args['text'])
utils.add_hook(handle_away, 'AWAY') utils.add_hook(handle_away, 'AWAY')
def handle_spawnmain(irc, numeric, command, args): def handle_spawnmain(irc, numeric, command, args):
@ -1039,16 +1039,16 @@ def handle_invite(irc, source, command, args):
remotechan = findRemoteChan(irc, remoteirc, channel) remotechan = findRemoteChan(irc, remoteirc, channel)
remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False) remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False)
if remotesource is None: if remotesource is None:
utils.msg(irc, source, 'Error: You must be in a common channel ' irc.msg(source, 'Error: You must be in a common channel '
'with %s to invite them to channels.' % \ 'with %s to invite them to channels.' % \
irc.users[target].nick, irc.users[target].nick,
notice=True) notice=True)
elif remotechan is None: elif remotechan is None:
utils.msg(irc, source, 'Error: You cannot invite someone to a ' irc.msg(source, 'Error: You cannot invite someone to a '
'channel not on their network!', 'channel not on their network!',
notice=True) notice=True)
else: else:
remoteirc.proto.inviteClient(remoteirc, remotesource, remoteuser, remoteirc.proto.inviteClient(remotesource, remoteuser,
remotechan) remotechan)
utils.add_hook(handle_invite, 'INVITE') utils.add_hook(handle_invite, 'INVITE')
@ -1060,43 +1060,43 @@ def linkacl(irc, source, args):
LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist.""" LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist."""
missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)." missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
if not utils.isOper(irc, source): if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') irc.msg(source, 'Error: You must be opered in order to complete this operation.')
return return
try: try:
cmd = args[0].lower() cmd = args[0].lower()
channel = utils.toLower(irc, args[1]) channel = utils.toLower(irc, args[1])
except IndexError: except IndexError:
utils.msg(irc, source, missingargs) irc.msg(source, missingargs)
return return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel) irc.msg(source, 'Error: Invalid channel %r.' % channel)
return return
relay = findRelay((irc.name, channel)) relay = findRelay((irc.name, channel))
if not relay: if not relay:
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel) irc.msg(source, 'Error: No such relay %r exists.' % channel)
return return
if cmd == 'list': if cmd == 'list':
s = 'Blocked networks for \x02%s\x02: \x02%s\x02' % (channel, ', '.join(db[relay]['blocked_nets']) or '(empty)') s = 'Blocked networks for \x02%s\x02: \x02%s\x02' % (channel, ', '.join(db[relay]['blocked_nets']) or '(empty)')
utils.msg(irc, source, s) irc.msg(source, s)
return return
try: try:
remotenet = args[2] remotenet = args[2]
except IndexError: except IndexError:
utils.msg(irc, source, missingargs) irc.msg(source, missingargs)
return return
if cmd == 'deny': if cmd == 'deny':
db[relay]['blocked_nets'].add(remotenet) db[relay]['blocked_nets'].add(remotenet)
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
elif cmd == 'allow': elif cmd == 'allow':
try: try:
db[relay]['blocked_nets'].remove(remotenet) db[relay]['blocked_nets'].remove(remotenet)
except KeyError: except KeyError:
utils.msg(irc, source, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel)) irc.msg(source, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel))
else: else:
utils.msg(irc, source, 'Done.') irc.msg(source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd) irc.msg(source, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
@utils.add_cmd @utils.add_cmd
def showuser(irc, source, args): def showuser(irc, source, args):
@ -1125,11 +1125,11 @@ def showuser(irc, source, args):
remotenet, remoteuser = r remotenet, remoteuser = r
remoteirc = world.networkobjects[remotenet] remoteirc = world.networkobjects[remotenet]
nicks.append('%s: \x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick)) nicks.append('%s: \x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
utils.msg(irc, source, "\x02Relay nicks\x02: %s" % ', '.join(nicks)) irc.msg(source, "\x02Relay nicks\x02: %s" % ', '.join(nicks))
relaychannels = [] relaychannels = []
for ch in irc.users[u].channels: for ch in irc.users[u].channels:
relay = findRelay((irc.name, ch)) relay = findRelay((irc.name, ch))
if relay: if relay:
relaychannels.append(''.join(relay)) relaychannels.append(''.join(relay))
if relaychannels and (utils.isOper(irc, source) or u == source): if relaychannels and (utils.isOper(irc, source) or u == source):
utils.msg(irc, source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels)) irc.msg(source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels))

View File

@ -2,98 +2,110 @@ import time
import sys import sys
import os import os
import re import re
from copy import copy
# Import hacks to access utils and classes...
curdir = os.path.dirname(__file__) curdir = os.path.dirname(__file__)
sys.path += [curdir, os.path.dirname(curdir)] sys.path += [curdir, os.path.dirname(curdir)]
import utils import utils
from log import log from log import log
from classes import * from classes import *
from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \ from ts6_common import TS6BaseProtocol
removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args
from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \
handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \
handle_notice, _send, handle_part
casemapping = 'rfc1459' class InspIRCdProtocol(TS6BaseProtocol):
def __init__(self, irc):
super(InspIRCdProtocol, self).__init__(irc)
# 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 # Raw commands sent from servers vary from protocol to protocol. Here, we map
# non-standard names to our hook handlers, so plugins get the information they need. # non-standard names to our hook handlers, so command handlers' outputs
# are called with the right hooks.
hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE',
'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST',
'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'}
def _send(irc, sid, msg): def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
irc.send(':%s %s' % (sid, msg))
def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None):
server = server or irc.sid """Spawns a client with nick <nick> on the given IRC connection.
if not utils.isInternalServer(irc, server):
Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid."""
server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# We need a separate UID generator instance for every PseudoServer # We need a separate UID generator instance for every PseudoServer
# we spawn. Otherwise, things won't wrap around properly. # we spawn. Otherwise, things won't wrap around properly.
if server not in irc.uidgen: if server not in self.irc.uidgen:
irc.uidgen[server] = utils.TS6UIDGenerator(server) self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = irc.uidgen[server].next_uid() uid = self.irc.uidgen[server].next_uid()
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or irc.botdata['realname'] realname = realname or self.irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes) utils.applyModes(self.irc, uid, modes)
irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
_send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
" {ts} {modes} + :{realname}".format(ts=ts, host=host, " {ts} {modes} + :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname, modes=raw_modes, ip=ip, realname=realname,
realhost=realhost)) realhost=realhost))
if ('o', None) in modes or ('+o', None) in modes: if ('o', None) in modes or ('+o', None) in modes:
_operUp(irc, uid, opertype=opertype or 'IRC_Operator') self._operUp(uid, opertype=opertype or 'IRC_Operator')
return u return u
def joinClient(irc, client, channel): def joinClient(self, client, channel):
"""Joins a PyLink client to a channel."""
# InspIRCd doesn't distinguish between burst joins and regular joins, # InspIRCd doesn't distinguish between burst joins and regular joins,
# so what we're actually doing here is sending FJOIN from the server, # so what we're actually doing here is sending FJOIN from the server,
# on behalf of the clients that call it. # on behalf of the clients that are joining.
channel = utils.toLower(irc, channel) channel = utils.toLower(self.irc, channel)
server = utils.isInternalClient(irc, client) server = utils.isInternalClient(self.irc, client)
if not server: if not server:
log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel)
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
# Strip out list-modes, they shouldn't be ever sent in FJOIN. # Strip out list-modes, they shouldn't be ever sent in FJOIN.
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
_send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format( self._send(server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
ts=irc.channels[channel].ts, uid=client, channel=channel, ts=self.irc.channels[channel].ts, uid=client, channel=channel,
modes=utils.joinModes(modes))) modes=utils.joinModes(modes)))
irc.channels[channel].users.add(client) self.irc.channels[channel].users.add(client)
irc.users[client].channels.add(channel) self.irc.users[client].channels.add(channel)
def sjoinServer(irc, server, channel, users, ts=None): def sjoinServer(self, server, channel, users, ts=None):
channel = utils.toLower(irc, channel) """Sends an SJOIN for a group of users to a channel.
server = server or irc.sid
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)])
"""
channel = utils.toLower(self.irc, channel)
server = server or self.irc.sid
assert users, "sjoinServer: No users sent?" assert users, "sjoinServer: No users sent?"
log.debug('(%s) sjoinServer: got %r for users', irc.name, users) log.debug('(%s) sjoinServer: got %r for users', self.irc.name, users)
if not server: if not server:
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
orig_ts = irc.channels[channel].ts orig_ts = self.irc.channels[channel].ts
ts = ts or orig_ts ts = ts or orig_ts
if ts < orig_ts: if ts < orig_ts:
# If the TS we're sending is lower than the one that existing, clear the # If the TS we're sending is lower than the one that existing, clear the
# mode lists from our channel state and reset the timestamp. # mode lists from our channel state and reset the timestamp.
log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)',
irc.name, channel, orig_ts, ts) self.irc.name, channel, orig_ts, ts)
irc.channels[channel].ts = ts self.irc.channels[channel].ts = ts
irc.channels[channel].modes.clear() self.irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values(): for p in self.irc.channels[channel].prefixmodes.values():
p.clear() p.clear()
log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts,
time.strftime("%c", time.localtime(ts))) time.strftime("%c", time.localtime(ts)))
# Strip out list-modes, they shouldn't be ever sent in FJOIN. # Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules).
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
uids = [] uids = []
changedmodes = [] changedmodes = []
namelist = [] namelist = []
@ -106,246 +118,203 @@ def sjoinServer(irc, server, channel, users, ts=None):
for m in prefixes: for m in prefixes:
changedmodes.append(('+%s' % m, user)) changedmodes.append(('+%s' % m, user))
try: try:
irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
except KeyError: # Not initialized yet? except KeyError: # Not initialized yet?
log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", irc.name, channel, user) log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(self.irc, channel, changedmodes)
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
_send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format( self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format(
ts=ts, users=namelist, channel=channel, ts=ts, users=namelist, channel=channel,
modes=utils.joinModes(modes))) modes=utils.joinModes(modes)))
irc.channels[channel].users.update(uids) self.irc.channels[channel].users.update(uids)
def _operUp(irc, target, opertype=None): def _operUp(self, target, opertype=None):
userobj = irc.users[target] """Opers a client up (internal function specific to InspIRCd).
This should be called whenever user mode +o is set on anyone, because
InspIRCd requires a special command (OPERTYPE) to be sent in order to
recognize ANY non-burst oper ups.
Plugins don't have to call this function themselves, but they can
set the opertype attribute of an IrcUser object (in self.irc.users),
and the change will be reflected here."""
userobj = self.irc.users[target]
try: try:
otype = opertype or userobj.opertype otype = opertype or userobj.opertype
except AttributeError: except AttributeError:
log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!',
irc.name, target, userobj.nick) self.irc.name, target, userobj.nick)
# whatever, this is non-standard anyways. # whatever, this is non-standard anyways.
otype = 'IRC_Operator' otype = 'IRC_Operator'
log.debug('(%s) Sending OPERTYPE from %s to oper them up.', log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
irc.name, target) self.irc.name, target)
userobj.opertype = otype userobj.opertype = otype
_send(irc, target, 'OPERTYPE %s' % otype) self._send(target, 'OPERTYPE %s' % otype)
def _sendModes(irc, numeric, target, modes, ts=None): def _sendModes(self, numeric, target, modes, ts=None):
"""Internal function to send mode changes from a PyLink client/server."""
# -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA
# -> :9PYAAAAAA MODE 9PYAAAAAA -i+w # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w
log.debug('(%s) inspircd._sendModes: received %r for mode list', irc.name, modes) log.debug('(%s) inspself.ircd._sendModes: received %r for mode list', self.irc.name, modes)
if ('+o', None) in modes and not utils.isChannel(target): if ('+o', None) in modes and not utils.isChannel(target):
# https://github.com/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # https://github.com/inspself.ircd/inspself.ircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28
# Servers need a special command to set umode +o on people. # Servers need a special command to set umode +o on people.
# Why isn't this documented anywhere, InspIRCd? # Why isn't this documented anywhere, InspIRCd?
_operUp(irc, target) self._operUp(target)
utils.applyModes(irc, target, modes) utils.applyModes(self.irc, target, modes)
joinedmodes = utils.joinModes(modes) joinedmodes = utils.joinModes(modes)
if utils.isChannel(target): if utils.isChannel(target):
ts = ts or irc.channels[utils.toLower(irc, target)].ts ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts
_send(irc, numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) self._send(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes))
else: else:
_send(irc, numeric, 'MODE %s %s' % (target, joinedmodes)) self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def modeClient(irc, numeric, target, modes, ts=None): def modeClient(self, numeric, target, modes, ts=None):
"""<irc object> <client numeric> <list of modes>
Sends modes from a PyLink PseudoClient. <list of modes> should be
a list of (mode, arg) tuples, in the format of utils.parseModes() output.
""" """
if not utils.isInternalClient(irc, numeric): Sends mode changes from a PyLink client. <modes> should be
a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output.
"""
if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_sendModes(irc, numeric, target, modes, ts=ts) self._sendModes(numeric, target, modes, ts=ts)
def modeServer(irc, numeric, target, modes, ts=None): def modeServer(self, numeric, target, modes, ts=None):
"""<irc object> <server SID> <list of modes>
Sends modes from a PyLink PseudoServer. <list of modes> should be
a list of (mode, arg) tuples, in the format of utils.parseModes() output.
""" """
if not utils.isInternalServer(irc, numeric): Sends mode changes from a PyLink server. <list of modes> should be
raise LookupError('No such PyLink PseudoServer exists.') a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output.
_sendModes(irc, numeric, target, modes, ts=ts)
def killServer(irc, numeric, target, reason):
"""<irc object> <server SID> <target> <reason>
Sends a kill to <target> from a PyLink PseudoServer.
""" """
if not utils.isInternalServer(irc, numeric): if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
_send(irc, numeric, 'KILL %s :%s' % (target, reason)) self._sendModes(numeric, target, modes, ts=ts)
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 # We don't need to call removeClient here, since the remote server
# will send a QUIT from the target if the command succeeds. # will send a QUIT from the target if the command succeeds.
def killClient(irc, numeric, target, reason): def killClient(self, numeric, target, reason):
"""<irc object> <client numeric> <target> <reason> """Sends a kill from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Sends a kill to <target> from a PyLink PseudoClient.
"""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'KILL %s :%s' % (target, reason)) self._send(numeric, 'KILL %s :%s' % (target, reason))
# We don't need to call removeClient here, since the remote server # We don't need to call removeClient here, since the remote server
# will send a QUIT from the target if the command succeeds. # will send a QUIT from the target if the command succeeds.
def topicServer(irc, numeric, target, text): def topicServer(self, numeric, target, text):
if not utils.isInternalServer(irc, numeric): """Sends a topic change from a PyLink server. This is usally used on burst."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
ts = int(time.time()) ts = int(time.time())
servername = irc.servers[numeric].name servername = self.irc.servers[numeric].name
_send(irc, numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) self._send(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text))
irc.channels[target].topic = text self.irc.channels[target].topic = text
irc.channels[target].topicset = True self.irc.channels[target].topicset = True
def inviteClient(irc, numeric, target, channel): def inviteClient(self, numeric, target, channel):
"""<irc object> <client numeric> <text> """Sends an INVITE from a PyLink client.."""
if not utils.isInternalClient(self.irc, numeric):
Invites <target> to <channel> to <text> from PyLink client <client numeric>."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'INVITE %s %s' % (target, channel)) self._send(numeric, 'INVITE %s %s' % (target, channel))
def knockClient(irc, numeric, target, text): def knockClient(self, numeric, target, text):
"""<irc object> <client numeric> <text> """Sends a KNOCK from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Knocks on <channel> with <text> from PyLink client <client numeric>."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'ENCAP * KNOCK %s :%s' % (target, text)) self._send(numeric, 'ENCAP * KNOCK %s :%s' % (target, text))
def updateClient(irc, numeric, field, text): def updateClient(self, numeric, field, text):
"""<irc object> <client numeric> <field> <text> """Updates the ident, host, or realname of a PyLink client."""
Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
field = field.upper() field = field.upper()
if field == 'IDENT': if field == 'IDENT':
irc.users[numeric].ident = text self.irc.users[numeric].ident = text
_send(irc, numeric, 'FIDENT %s' % text) self._send(numeric, 'FIDENT %s' % text)
elif field == 'HOST': elif field == 'HOST':
irc.users[numeric].host = text self.irc.users[numeric].host = text
_send(irc, numeric, 'FHOST %s' % text) self._send(numeric, 'FHOST %s' % text)
elif field in ('REALNAME', 'GECOS'): elif field in ('REALNAME', 'GECOS'):
irc.users[numeric].realname = text self.irc.users[numeric].realname = text
_send(irc, numeric, 'FNAME :%s' % text) self._send(numeric, 'FNAME :%s' % text)
else: else:
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
def pingServer(irc, source=None, target=None): def pingServer(self, source=None, target=None):
source = source or irc.sid """Sends a PING to a target server. Periodic PINGs are sent to our uplink
target = target or irc.uplink automatically by the Irc() internals; plugins shouldn't have to use this."""
source = source or self.irc.sid
target = target or self.irc.uplink
if not (target is None or source is None): if not (target is None or source is None):
_send(irc, source, 'PING %s %s' % (source, target)) self._send(source, 'PING %s %s' % (source, target))
def numericServer(irc, source, numeric, text): def numericServer(self, source, numeric, text):
raise NotImplementedError("Numeric sending is not yet implemented by this " raise NotImplementedError("Numeric sending is not yet implemented by this "
"protocol module. WHOIS requests are handled " "protocol module. WHOIS requests are handled "
"locally by InspIRCd servers, so there is no " "locally by InspIRCd servers, so there is no "
"need for PyLink to send numerics directly yet.") "need for PyLink to send numerics directly yet.")
def awayClient(irc, source, text): def awayClient(self, source, text):
"""<irc object> <numeric> <text> """Sends an AWAY message from a PyLink client. <text> can be an empty string
to unset AWAY status."""
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text: if text:
_send(irc, source, 'AWAY %s :%s' % (int(time.time()), text)) self._send(source, 'AWAY %s :%s' % (int(time.time()), text))
else: else:
_send(irc, source, 'AWAY') self._send(source, 'AWAY')
def connect(irc): def spawnServer(self, name, sid=None, uplink=None, desc=None):
ts = irc.start_ts """Spawns a server off a PyLink server."""
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
uplink = uplink or self.irc.sid
name = name.lower()
# "desc" defaults to the configured server description.
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
if sid is None: # No sid given; generate one!
self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"])
sid = self.irc.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
for server in self.irc.servers.values():
if name == server.name:
raise ValueError('A server named %r already exists!' % name)
if not utils.isInternalServer(self.irc, uplink):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink)
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
self.irc.servers[sid] = IrcServer(uplink, name, internal=True)
self._send(sid, 'ENDBURST')
return sid
f = irc.send def squitServer(self, source, target, text='No reason given'):
"""SQUITs a PyLink server."""
# -> :9PY SQUIT 9PZ :blah, blah
self._send(source, 'SQUIT %s :%s' % (target, text))
self.handle_squit(source, 'SQUIT', [target, text])
def connect(self):
"""Initializes a connection to a server."""
ts = self.irc.start_ts
f = self.irc.send
f('CAPAB START 1202') f('CAPAB START 1202')
f('CAPAB CAPABILITIES :PROTOCOL=1202') f('CAPAB CAPABILITIES :PROTOCOL=1202')
f('CAPAB END') f('CAPAB END')
f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=irc.serverdata["hostname"], f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=self.irc.serverdata["hostname"],
Pass=irc.serverdata["sendpass"], sid=irc.sid, Pass=self.irc.serverdata["sendpass"], sid=self.irc.sid,
sdesc=irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) sdesc=self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']))
f(':%s BURST %s' % (irc.sid, ts)) f(':%s BURST %s' % (self.irc.sid, ts))
f(':%s ENDBURST' % (irc.sid)) f(':%s ENDBURST' % (self.irc.sid))
def handle_ping(irc, source, command, args): def handle_events(self, data):
# <- :70M PING 70M 0AL """Event handler for the InspIRCd protocol.
# -> :0AL PONG 0AL 70M
if utils.isInternalServer(irc, args[1]):
_send(irc, args[1], 'PONG %s %s' % (args[1], source))
def handle_pong(irc, source, command, args): This passes most commands to the various handle_ABCD() functions
if source == irc.uplink and args[1] == irc.sid: elsewhere in this module, but also handles commands sent in the
irc.lastping = time.time() initial server linking phase."""
def handle_fjoin(irc, servernumeric, command, args):
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
channel = utils.toLower(irc, args[0])
# InspIRCd sends each user's channel data in the form of 'modeprefix(es),UID'
userlist = args[-1].split()
our_ts = irc.channels[channel].ts
their_ts = int(args[1])
if their_ts < our_ts:
# Channel timestamp was reset on burst
log.debug('(%s) Setting channel TS of %s to %s from %s',
irc.name, channel, their_ts, our_ts)
irc.channels[channel].ts = their_ts
irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values():
p.clear()
modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(irc, channel, modestring)
utils.applyModes(irc, channel, parsedmodes)
namelist = []
for user in userlist:
modeprefix, user = user.split(',', 1)
namelist.append(user)
irc.users[user].channels.add(channel)
if their_ts <= our_ts:
utils.applyModes(irc, channel, [('+%s' % mode, user) for mode in modeprefix])
irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
def handle_uid(irc, numeric, command, args):
# :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
uid, ts, nick, realhost, host, ident, ip = args[0:7]
realname = args[-1]
irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]])
log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes)
irc.servers[numeric].users.add(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_server(irc, numeric, command, args):
# SERVER is sent by our uplink or any other server to introduce others.
# <- :00A SERVER test.server * 1 00C :testing raw message syntax
# <- :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California)
servername = args[0].lower()
sid = args[3]
sdesc = args[-1]
irc.servers[sid] = IrcServer(numeric, servername)
return {'name': servername, 'sid': args[3], 'text': sdesc}
def handle_fmode(irc, numeric, command, args):
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
channel = utils.toLower(irc, args[0])
modes = args[2:]
changedmodes = utils.parseModes(irc, channel, modes)
utils.applyModes(irc, channel, changedmodes)
ts = int(args[1])
return {'target': channel, 'modes': changedmodes, 'ts': ts}
def handle_idle(irc, numeric, command, args):
"""Handle the IDLE command, sent between servers in remote WHOIS queries."""
# <- :70MAAAAAA IDLE 1MLAAAAIG
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
sourceuser = numeric
targetuser = args[0]
_send(irc, targetuser, 'IDLE %s %s 0' % (sourceuser, irc.users[targetuser].ts))
def handle_events(irc, data):
# Each server message looks something like this: # Each server message looks something like this:
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE
# :<sid> <command> <argument1> <argument2> ... :final multi word argument # :<sid> <command> <argument1> <argument2> ... :final multi word argument
@ -357,11 +326,11 @@ def handle_events(irc, data):
# <- SERVER whatever.net abcdefgh 0 10X :something # <- SERVER whatever.net abcdefgh 0 10X :something
servername = args[1].lower() servername = args[1].lower()
numeric = args[4] numeric = args[4]
if args[2] != irc.serverdata['recvpass']: if args[2] != self.irc.serverdata['recvpass']:
# Check if recvpass is correct # Check if recvpass is correct
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
irc.servers[numeric] = IrcServer(None, servername) self.irc.servers[numeric] = IrcServer(None, servername)
irc.uplink = numeric self.irc.uplink = numeric
return return
elif args[0] == 'CAPAB': elif args[0] == 'CAPAB':
# Capability negotiation with our uplink # Capability negotiation with our uplink
@ -379,36 +348,36 @@ def handle_events(irc, data):
# (the former is config default, but I personally prefer the latter.) # (the former is config default, but I personally prefer the latter.)
name = 'owner' name = 'owner'
# We don't really care about mode prefixes; just the mode char # We don't really care about mode prefixes; just the mode char
irc.cmodes[name.lstrip(':')] = char[-1] self.irc.cmodes[name.lstrip(':')] = char[-1]
elif args[1] == 'USERMODES': elif args[1] == 'USERMODES':
# <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S wallops=w # <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S wallops=w
# Ditto above. # Ditto above.
for modepair in args[2:]: for modepair in args[2:]:
name, char = modepair.split('=') name, char = modepair.split('=')
irc.umodes[name.lstrip(':')] = char self.irc.umodes[name.lstrip(':')] = char
elif args[1] == 'CAPABILITIES': elif args[1] == 'CAPABILITIES':
# <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128 MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+ CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1 # <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128 MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+ CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1
caps = dict([x.lstrip(':').split('=') for x in args[2:]]) caps = dict([x.lstrip(':').split('=') for x in args[2:]])
protocol_version = int(caps['PROTOCOL']) protocol_version = int(caps['PROTOCOL'])
if protocol_version < 1202: if protocol_version < 1202:
raise ProtocolError("Remote protocol version is too old! At least 1202 (InspIRCd 2.0.x) is needed. (got %s)" % protocol_version) raise ProtocolError("Remote protocol version is too old! At least 1202 (InspIRCd 2.0.x) is needed. (got %s)" % protocol_version)
irc.maxnicklen = int(caps['NICKMAX']) self.irc.maxnicklen = int(caps['NICKMAX'])
irc.maxchanlen = int(caps['CHANMAX']) self.irc.maxchanlen = int(caps['CHANMAX'])
# Modes are divided into A, B, C, and D classes # Modes are divided into A, B, C, and D classes
# See http://www.irc.org/tech_docs/005.html # See http://www.self.irc.org/tech_docs/005.html
# FIXME: Find a better way to assign/store this. # FIXME: Find a better way to assign/store this.
irc.cmodes['*A'], irc.cmodes['*B'], irc.cmodes['*C'], irc.cmodes['*D'] \ self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] \
= caps['CHANMODES'].split(',') = caps['CHANMODES'].split(',')
irc.umodes['*A'], irc.umodes['*B'], irc.umodes['*C'], irc.umodes['*D'] \ self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] \
= caps['USERMODES'].split(',') = caps['USERMODES'].split(',')
prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', caps['PREFIX']) prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', caps['PREFIX'])
irc.prefixmodes = dict(zip(prefixsearch.group(1), prefixsearch.group(2))) self.irc.prefixmodes = dict(zip(prefixsearch.group(1), prefixsearch.group(2)))
log.debug('(%s) irc.prefixmodes set to %r', irc.name, irc.prefixmodes) log.debug('(%s) self.irc.prefixmodes set to %r', self.irc.name, self.irc.prefixmodes)
# Sanity check: set this AFTER we fetch the capabilities for the network! # Sanity check: set this AFTER we fetch the capabilities for the network!
irc.connected.set() self.irc.connected.set()
try: try:
args = parseTS6Args(args) args = self.parseTS6Args(args)
numeric = args[0] numeric = args[0]
command = args[1] command = args[1]
args = args[2:] args = args[2:]
@ -417,62 +386,138 @@ def handle_events(irc, data):
# We will do wildcard event handling here. Unhandled events are just ignored. # We will do wildcard event handling here. Unhandled events are just ignored.
try: try:
func = globals()['handle_'+command.lower()] func = getattr(self, 'handle_'+command.lower())
except KeyError: # unhandled event except AttributeError: # unhandled event
pass pass
else: else:
parsed_args = func(irc, numeric, command, args) parsed_args = func(numeric, command, args)
if parsed_args is not None: if parsed_args is not None:
return [numeric, command, parsed_args] return [numeric, command, parsed_args]
def spawnServer(irc, name, sid=None, uplink=None, desc=None): def handle_ping(self, source, command, args):
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver """Handles incoming PING commands, so we don't time out."""
uplink = uplink or irc.sid # <- :70M PING 70M 0AL
name = name.lower() # -> :0AL PONG 0AL 70M
desc = desc or irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'] if utils.isInternalServer(self.irc, args[1]):
if sid is None: # No sid given; generate one! self._send(args[1], 'PONG %s %s' % (args[1], source))
irc.sidgen = utils.TS6SIDGenerator(irc.serverdata["sidrange"])
sid = irc.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
for server in irc.servers.values():
if name == server.name:
raise ValueError('A server named %r already exists!' % name)
if not utils.isInternalServer(irc, uplink):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink)
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
_send(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
irc.servers[sid] = IrcServer(uplink, name, internal=True)
_send(irc, sid, 'ENDBURST')
return sid
def squitServer(irc, source, target, text='No reason given'): def handle_pong(self, source, command, args):
# -> :9PY SQUIT 9PZ :blah, blah """Handles incoming PONG commands.
_send(irc, source, 'SQUIT %s :%s' % (target, text))
handle_squit(irc, source, 'SQUIT', [target, text])
def handle_ftopic(irc, numeric, command, args): This is used to keep track of whether the uplink is alive by the Irc()
internals - a server that fails to reply to our PINGs eventually
times out and is disconnected."""
if source == self.irc.uplink and args[1] == self.irc.sid:
self.irc.lastping = time.time()
def handle_fjoin(self, servernumeric, command, args):
"""Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN)."""
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
channel = utils.toLower(self.irc, args[0])
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
userlist = args[-1].split()
our_ts = self.irc.channels[channel].ts
their_ts = int(args[1])
if their_ts < our_ts:
# Channel timestamp was reset on burst
log.debug('(%s) Setting channel TS of %s to %s from %s',
self.irc.name, channel, their_ts, our_ts)
self.irc.channels[channel].ts = their_ts
self.irc.channels[channel].modes.clear()
for p in self.irc.channels[channel].prefixmodes.values():
p.clear()
modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(self.irc, channel, modestring)
utils.applyModes(self.irc, channel, parsedmodes)
namelist = []
for user in userlist:
modeprefix, user = user.split(',', 1)
namelist.append(user)
self.irc.users[user].channels.add(channel)
if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in modeprefix])
self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
def handle_uid(self, numeric, command, args):
"""Handles incoming UID commands (user introduction)."""
# :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
uid, ts, nick, realhost, host, ident, ip = args[0:7]
realname = args[-1]
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(self.irc, uid, [args[8], args[9]])
log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(self.irc, uid, parsedmodes)
self.irc.servers[numeric].users.add(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_server(self, numeric, command, args):
"""Handles incoming SERVER commands (introduction of servers)."""
# SERVER is sent by our uplink or any other server to introduce others.
# <- :00A SERVER test.server * 1 00C :testing raw message syntax
# <- :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California)
servername = args[0].lower()
sid = args[3]
sdesc = args[-1]
self.irc.servers[sid] = IrcServer(numeric, servername)
return {'name': servername, 'sid': args[3], 'text': sdesc}
def handle_fmode(self, numeric, command, args):
"""Handles the FMODE command, used for channel mode changes."""
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
channel = utils.toLower(self.irc, args[0])
modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes)
utils.applyModes(self.irc, channel, changedmodes)
ts = int(args[1])
return {'target': channel, 'modes': changedmodes, 'ts': ts}
def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes."""
# In InspIRCd, MODE is used for setting user modes and
# FMODE is used for channel modes:
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0]
modestrings = args[1:]
changedmodes = utils.parseModes(self.irc, numeric, modestrings)
utils.applyModes(self.irc, target, changedmodes)
return {'target': target, 'modes': changedmodes}
def handle_idle(self, numeric, command, args):
"""Handles the IDLE command, sent between servers in remote WHOIS queries."""
# <- :70MAAAAAA IDLE 1MLAAAAIG
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
sourceuser = numeric
targetuser = args[0]
self._send(targetuser, 'IDLE %s %s 0' % (sourceuser, self.irc.users[targetuser].ts))
def handle_ftopic(self, numeric, command, args):
"""Handles incoming FTOPIC (sets topic on burst)."""
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic # <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic
channel = utils.toLower(irc, args[0]) channel = utils.toLower(self.irc, args[0])
ts = args[1] ts = args[1]
setter = args[2] setter = args[2]
topic = args[-1] topic = args[-1]
irc.channels[channel].topic = topic self.irc.channels[channel].topic = topic
irc.channels[channel].topicset = True self.irc.channels[channel].topicset = True
return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic}
def handle_invite(irc, numeric, command, args): def handle_invite(self, numeric, command, args):
"""Handles incoming INVITEs."""
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0
target = args[0] target = args[0]
channel = utils.toLower(irc, args[1]) channel = utils.toLower(self.irc, args[1])
# We don't actually need to process this; it's just something plugins/hooks can use # We don't actually need to process this; just send the hook so plugins can use it
return {'target': target, 'channel': channel} return {'target': target, 'channel': channel}
def handle_encap(irc, numeric, command, args): def handle_encap(self, numeric, command, args):
"""Handles incoming encapsulated commands (ENCAP). Hook arguments
returned by this should have a parse_as field, that sets the correct
hook name for the message.
For InspIRCd, the only ENCAP command we handle right now is KNOCK."""
# <- :70MAAAAAA ENCAP * KNOCK #blah :agsdfas # <- :70MAAAAAA ENCAP * KNOCK #blah :agsdfas
# From charybdis TS6 docs: https://github.com/grawity/irc-docs/blob/03ba884a54f1cef2193cd62b6a86803d89c1ac41/server/ts6.txt # From charybdis TS6 docs: https://github.com/grawity/self.irc-docs/blob/03ba884a54f1cef2193cd62b6a86803d89c1ac41/server/ts6.txt
# ENCAP # ENCAP
# source: any # source: any
@ -484,47 +529,58 @@ def handle_encap(irc, numeric, command, args):
targetmask = args[0] targetmask = args[0]
real_command = args[1] real_command = args[1]
if targetmask == '*' and real_command == 'KNOCK': if targetmask == '*' and real_command == 'KNOCK':
channel = utils.toLower(irc, args[2]) channel = utils.toLower(self.irc, args[2])
text = args[3] text = args[3]
return {'parse_as': real_command, 'channel': channel, return {'parse_as': real_command, 'channel': channel,
'text': text} 'text': text}
def handle_opertype(irc, numeric, command, args): def handle_opertype(self, numeric, command, args):
"""Handles incoming OPERTYPE, which is used to denote an oper up.
This calls the internal hook PYLINK_CLIENT_OPERED, sets the internal
opertype of the client, and assumes setting user mode +o on the caller."""
# This is used by InspIRCd to denote an oper up; there is no MODE # This is used by InspIRCd to denote an oper up; there is no MODE
# command sent for it. # command sent for it.
# <- :70MAAAAAB OPERTYPE Network_Owner # <- :70MAAAAAB OPERTYPE Network_Owner
omode = [('+o', None)] omode = [('+o', None)]
irc.users[numeric].opertype = opertype = args[0] self.irc.users[numeric].opertype = opertype = args[0]
utils.applyModes(irc, numeric, omode) utils.applyModes(self.irc, numeric, omode)
# OPERTYPE is essentially umode +o and metadata in one command; # OPERTYPE is essentially umode +o and metadata in one command;
# we'll call that too. # we'll call that too.
irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}]) self.irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}])
return {'target': numeric, 'modes': omode} return {'target': numeric, 'modes': omode}
def handle_fident(irc, numeric, command, args): def handle_fident(self, numeric, command, args):
# :70MAAAAAB FHOST test """Handles FIDENT, used for denoting ident changes."""
# :70MAAAAAB FNAME :afdsafasf # <- :70MAAAAAB FIDENT test
# :70MAAAAAB FIDENT test self.irc.users[numeric].ident = newident = args[0]
irc.users[numeric].ident = newident = args[0]
return {'target': numeric, 'newident': newident} return {'target': numeric, 'newident': newident}
def handle_fhost(irc, numeric, command, args): def handle_fhost(self, numeric, command, args):
irc.users[numeric].host = newhost = args[0] """Handles FHOST, used for denoting hostname changes."""
# <- :70MAAAAAB FIDENT some.host
self.irc.users[numeric].host = newhost = args[0]
return {'target': numeric, 'newhost': newhost} return {'target': numeric, 'newhost': newhost}
def handle_fname(irc, numeric, command, args): def handle_fname(self, numeric, command, args):
irc.users[numeric].realname = newgecos = args[0] """Handles FNAME, used for denoting real name/gecos changes."""
# <- :70MAAAAAB FNAME :afdsafasf
self.irc.users[numeric].realname = newgecos = args[0]
return {'target': numeric, 'newgecos': newgecos} return {'target': numeric, 'newgecos': newgecos}
def handle_endburst(irc, numeric, command, args): def handle_endburst(self, numeric, command, args):
"""ENDBURST handler; sends a hook with empty contents."""
return {} return {}
def handle_away(irc, numeric, command, args): def handle_away(self, numeric, command, args):
"""Handles incoming AWAY messages."""
# <- :1MLAAAAIG AWAY 1439371390 :Auto-away # <- :1MLAAAAIG AWAY 1439371390 :Auto-away
try: try:
ts = args[0] ts = args[0]
irc.users[numeric].away = text = args[1] self.irc.users[numeric].away = text = args[1]
return {'text': text, 'ts': ts} return {'text': text, 'ts': ts}
except IndexError: # User is unsetting away status except IndexError: # User is unsetting away status
irc.users[numeric].away = '' self.irc.users[numeric].away = ''
return {'text': ''} return {'text': ''}
Class = InspIRCdProtocol

View File

@ -3,62 +3,76 @@ import sys
import os import os
import re import re
# Import hacks to access utils and classes...
curdir = os.path.dirname(__file__) curdir = os.path.dirname(__file__)
sys.path += [curdir, os.path.dirname(curdir)] sys.path += [curdir, os.path.dirname(curdir)]
import utils import utils
from log import log from log import log
from classes import * from classes import *
# Shared with inspircd module because the output is the same. from ts6_common import TS6BaseProtocol
from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \
removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args
from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \
handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \
handle_notice, _send, handle_part
casemapping = 'rfc1459' class TS6Protocol(TS6BaseProtocol):
hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} def __init__(self, irc):
super(TS6Protocol, self).__init__(irc)
self.casemapping = 'rfc1459'
self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'}
def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None):
server = server or irc.sid """Spawns a client with nick <nick> on the given IRC connection.
if not utils.isInternalServer(irc, server):
Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid."""
server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# We need a separate UID generator instance for every PseudoServer # We need a separate UID generator instance for every PseudoServer
# we spawn. Otherwise, things won't wrap around properly. # we spawn. Otherwise, things won't wrap around properly.
if server not in irc.uidgen: if server not in self.irc.uidgen:
irc.uidgen[server] = utils.TS6UIDGenerator(server) self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = irc.uidgen[server].next_uid() uid = self.irc.uidgen[server].next_uid()
# EUID: # EUID:
# parameters: nickname, hopcount, nickTS, umodes, username, # parameters: nickname, hopcount, nickTS, umodes, username,
# visible hostname, IP address, UID, real hostname, account name, gecos # visible hostname, IP address, UID, real hostname, account name, gecos
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or irc.botdata['realname'] realname = realname or self.irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes) utils.applyModes(self.irc, uid, modes)
irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
_send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
"{realhost} * :{realname}".format(ts=ts, host=host, "{realhost} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname, modes=raw_modes, ip=ip, realname=realname,
realhost=realhost)) realhost=realhost))
return u return u
def joinClient(irc, client, channel): def joinClient(self, client, channel):
channel = utils.toLower(irc, channel) """Joins a PyLink client to a channel."""
channel = utils.toLower(self.irc, channel)
# JOIN: # JOIN:
# parameters: channelTS, channel, '+' (a plus sign) # parameters: channelTS, channel, '+' (a plus sign)
if not utils.isInternalClient(irc, client): if not utils.isInternalClient(self.irc, client):
log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel)
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, client, "JOIN {ts} {channel} +".format(ts=irc.channels[channel].ts, channel=channel)) self._send(client, "JOIN {ts} {channel} +".format(ts=self.irc.channels[channel].ts, channel=channel))
irc.channels[channel].users.add(client) self.irc.channels[channel].users.add(client)
irc.users[client].channels.add(channel) self.irc.users[client].channels.add(channel)
def sjoinServer(irc, server, channel, users, ts=None): def 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'), ('o', 100AAABBB'), ('v', '100AAADDD')])
sjoinServer(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])
"""
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
@ -68,26 +82,26 @@ def sjoinServer(irc, server, channel, users, ts=None):
# their status ('@+', '@', '+' or ''), for example: # their status ('@+', '@', '+' or ''), for example:
# '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server # '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server
# so it is not possible to use this message to force users to join a channel. # so it is not possible to use this message to force users to join a channel.
channel = utils.toLower(irc, channel) channel = utils.toLower(self.irc, channel)
server = server or irc.sid server = server or self.irc.sid
assert users, "sjoinServer: No users sent?" assert users, "sjoinServer: No users sent?"
log.debug('(%s) sjoinServer: got %r for users', irc.name, users) log.debug('(%s) sjoinServer: got %r for users', self.irc.name, users)
if not server: if not server:
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
orig_ts = irc.channels[channel].ts orig_ts = self.irc.channels[channel].ts
ts = ts or orig_ts ts = ts or orig_ts
if ts < orig_ts: if ts < orig_ts:
# If the TS we're sending is lower than the one that existing, clear the # If the TS we're sending is lower than the one that existing, clear the
# mode lists from our channel state and reset the timestamp. # mode lists from our channel state and reset the timestamp.
log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)',
irc.name, channel, orig_ts, ts) self.irc.name, channel, orig_ts, ts)
irc.channels[channel].ts = ts self.irc.channels[channel].ts = ts
irc.channels[channel].modes.clear() self.irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values(): for p in self.irc.channels[channel].prefixmodes.values():
p.clear() p.clear()
log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts,
time.strftime("%c", time.localtime(ts))) time.strftime("%c", time.localtime(ts)))
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
changedmodes = [] changedmodes = []
while users[:10]: while users[:10]:
uids = [] uids = []
@ -98,69 +112,65 @@ def sjoinServer(irc, server, channel, users, ts=None):
prefixes, user = userpair prefixes, user = userpair
prefixchars = '' prefixchars = ''
for prefix in prefixes: for prefix in prefixes:
pr = irc.prefixmodes.get(prefix) pr = self.irc.prefixmodes.get(prefix)
if pr: if pr:
prefixchars += pr prefixchars += pr
changedmodes.append(('+%s' % prefix, user)) changedmodes.append(('+%s' % prefix, user))
namelist.append(prefixchars+user) namelist.append(prefixchars+user)
uids.append(user) uids.append(user)
try: try:
irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
except KeyError: # Not initialized yet? except KeyError: # Not initialized yet?
log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", irc.name, channel, user) log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
users = users[10:] users = users[10:]
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
_send(irc, server, "SJOIN {ts} {channel} {modes} :{users}".format( self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format(
ts=ts, users=namelist, channel=channel, ts=ts, users=namelist, channel=channel,
modes=utils.joinModes(modes))) modes=utils.joinModes(modes)))
irc.channels[channel].users.update(uids) self.irc.channels[channel].users.update(uids)
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(self.irc, channel, changedmodes)
def _sendModes(irc, numeric, target, modes, ts=None): def _sendModes(self, numeric, target, modes, ts=None):
utils.applyModes(irc, target, modes) """Internal function to send mode changes from a PyLink client/server."""
utils.applyModes(self.irc, target, modes)
if utils.isChannel(target): if utils.isChannel(target):
ts = ts or irc.channels[utils.toLower(irc, target)].ts ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts
# TMODE: # TMODE:
# parameters: channelTS, channel, cmode changes, opt. cmode parameters... # parameters: channelTS, channel, cmode changes, opt. cmode parameters...
# On output, at most ten cmode parameters should be sent; if there are more, # On output, at most ten cmode parameters should be sent; if there are more,
# multiple TMODE messages should be sent. # multiple TMODE messages should be sent.
while modes[:9]: while modes[:9]:
joinedmodes = utils.joinModes(modes = [m for m in modes[:9] if m[0] not in irc.cmodes['*A']]) joinedmodes = utils.joinModes(modes = [m for m in modes[:9] if m[0] not in self.irc.cmodes['*A']])
modes = modes[9:] modes = modes[9:]
_send(irc, numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes)) self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes))
else: else:
joinedmodes = utils.joinModes(modes) joinedmodes = utils.joinModes(modes)
_send(irc, numeric, 'MODE %s %s' % (target, joinedmodes)) self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def modeClient(irc, numeric, target, modes, ts=None): def modeClient(self, numeric, target, modes, ts=None):
"""<irc object> <client numeric> <list of modes>
Sends modes from a PyLink PseudoClient. <list of modes> should be
a list of (mode, arg) tuples, in the format of utils.parseModes() output.
""" """
if not utils.isInternalClient(irc, numeric): Sends mode changes from a PyLink client. <modes> should be
a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output.
"""
if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_sendModes(irc, numeric, target, modes, ts=ts) self._sendModes(numeric, target, modes, ts=ts)
def modeServer(irc, numeric, target, modes, ts=None): def modeServer(self, numeric, target, modes, ts=None):
"""<irc object> <server SID> <list of modes>
Sends modes from a PyLink PseudoServer. <list of modes> should be
a list of (mode, arg) tuples, in the format of utils.parseModes() output.
""" """
if not utils.isInternalServer(irc, numeric): Sends mode changes from a PyLink server. <list of modes> should be
a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output.
"""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
_sendModes(irc, numeric, target, modes, ts=ts) self._sendModes(numeric, target, modes, ts=ts)
def killServer(irc, numeric, target, reason): def killServer(self, numeric, target, reason):
"""<irc object> <server SID> <target> <reason> """Sends a kill from a PyLink server."""
if not utils.isInternalServer(self.irc, numeric):
Sends a kill to <target> from a PyLink PseudoServer.
"""
if not utils.isInternalServer(irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
# KILL: # KILL:
# parameters: target user, path # parameters: target user, path
@ -169,93 +179,117 @@ def killServer(irc, numeric, target, reason):
# the kill followed by a space and a parenthesized reason. To avoid overflow, # the kill followed by a space and a parenthesized reason. To avoid overflow,
# it is recommended not to add anything to the path. # it is recommended not to add anything to the path.
assert target in irc.users, "Unknown target %r for killServer!" % target assert target in self.irc.users, "Unknown target %r for killServer!" % target
_send(irc, numeric, 'KILL %s :Killed (%s)' % (target, reason)) self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason))
removeClient(irc, target) removeClient(self.irc, target)
def killClient(irc, numeric, target, reason): def killClient(self, numeric, target, reason):
"""<irc object> <client numeric> <target> <reason> """Sends a kill from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Sends a kill to <target> from a PyLink PseudoClient.
"""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
assert target in irc.users, "Unknown target %r for killClient!" % target assert target in self.irc.users, "Unknown target %r for killClient!" % target
_send(irc, numeric, 'KILL %s :Killed (%s)' % (target, reason)) self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason))
removeClient(irc, target) removeClient(self.irc, target)
def topicServer(irc, numeric, target, text): def topicServer(self, numeric, target, text):
if not utils.isInternalServer(irc, numeric): """Sends a topic change from a PyLink server. This is usally used on burst."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
# TB # TB
# capab: TB # capab: TB
# source: server # source: server
# propagation: broadcast # propagation: broadcast
# parameters: channel, topicTS, opt. topic setter, topic # parameters: channel, topicTS, opt. topic setter, topic
ts = irc.channels[target].ts ts = self.irc.channels[target].ts
servername = irc.servers[numeric].name servername = self.irc.servers[numeric].name
_send(irc, numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) self._send(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text))
irc.channels[target].topic = text self.irc.channels[target].topic = text
irc.channels[target].topicset = True self.irc.channels[target].topicset = True
def inviteClient(irc, numeric, target, channel): def inviteClient(self, numeric, target, channel):
"""<irc object> <client numeric> <text> """Sends an INVITE from a PyLink client.."""
if not utils.isInternalClient(self.irc, numeric):
Invites <target> to <channel> to <text> from PyLink client <client numeric>."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'INVITE %s %s %s' % (target, channel, irc.channels[channel].ts)) self._send(numeric, 'INVITE %s %s %s' % (target, channel, self.irc.channels[channel].ts))
def knockClient(irc, numeric, target, text): def knockClient(self, numeric, target, text):
"""<irc object> <client numeric> <text> """Sends a KNOCK from a PyLink client."""
if 'KNOCK' not in self.irc.caps:
Knocks on <channel> with <text> from PyLink client <client numeric>."""
if 'KNOCK' not in irc.caps:
log.debug('(%s) knockClient: Dropping KNOCK to %r since the IRCd ' log.debug('(%s) knockClient: Dropping KNOCK to %r since the IRCd '
'doesn\'t support it.', irc.name, target) 'doesn\'t support it.', self.irc.name, target)
return return
if not utils.isInternalClient(irc, numeric): if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
# No text value is supported here; drop it. # No text value is supported here; drop it.
_send(irc, numeric, 'KNOCK %s' % target) self._send(numeric, 'KNOCK %s' % target)
def updateClient(irc, numeric, field, text): def updateClient(self, numeric, field, text):
"""<irc object> <client numeric> <field> <text> """Updates the hostname of a PyLink client."""
Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
field = field.upper() field = field.upper()
if field == 'HOST': if field == 'HOST':
irc.users[numeric].host = text self.irc.users[numeric].host = text
_send(irc, irc.sid, 'CHGHOST %s :%s' % (numeric, text)) self._send(self.irc.sid, 'CHGHOST %s :%s' % (numeric, text))
else: else:
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
def pingServer(irc, source=None, target=None): def pingServer(self, source=None, target=None):
source = source or irc.sid """Sends a PING to a target server. Periodic PINGs are sent to our uplink
automatically by the Irc() internals; plugins shouldn't have to use this."""
source = source or self.irc.sid
if source is None: if source is None:
return return
if target is not None: if target is not None:
_send(irc, source, 'PING %s %s' % (source, target)) self._send(source, 'PING %s %s' % (source, target))
else: else:
_send(irc, source, 'PING %s' % source) self._send(source, 'PING %s' % source)
def numericServer(irc, source, numeric, target, text): def numericServer(self, source, numeric, target, text):
_send(irc, source, '%s %s %s' % (numeric, target, text)) """Sends raw numerics from a server to a remote client, used for WHOIS
replies."""
self._send(source, '%s %s %s' % (numeric, target, text))
def awayClient(irc, source, text): def awayClient(self, source, text):
"""<irc object> <numeric> <text> """Sends an AWAY message from a PyLink client. <text> can be an empty string
to unset AWAY status."""
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text: if text:
_send(irc, source, 'AWAY :%s' % text) self._send(source, 'AWAY :%s' % text)
else: else:
_send(irc, source, 'AWAY') self._send(source, 'AWAY')
def connect(irc): def spawnServer(self, name, sid=None, uplink=None, desc=None):
ts = irc.start_ts """Spawns a server off a PyLink server."""
# -> :0AL SID test.server 1 0XY :some silly pseudoserver
uplink = uplink or self.irc.sid
name = name.lower()
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
if sid is None: # No sid given; generate one!
self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"])
sid = self.irc.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
for server in self.irc.servers.values():
if name == server.name:
raise ValueError('A server named %r already exists!' % name)
if not utils.isInternalServer(self.irc, uplink):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink)
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc))
self.irc.servers[sid] = IrcServer(uplink, name, internal=True)
return sid
f = irc.send def squitServer(self, source, target, text='No reason given'):
"""SQUITs a PyLink server."""
# -> SQUIT 9PZ :blah, blah
self.irc.send('SQUIT %s :%s' % (target, text))
self.handle_squit(source, 'SQUIT', [target, text])
def connect(self):
"""Initializes a connection to a server."""
ts = self.irc.start_ts
f = self.irc.send
# Valid keywords (from mostly InspIRCd's named modes): # Valid keywords (from mostly InspIRCd's named modes):
# admin allowinvite autoop ban banexception blockcolor # admin allowinvite autoop ban banexception blockcolor
# c_registered exemptchanops filter forward flood halfop history invex # c_registered exemptchanops filter forward flood halfop history invex
@ -283,17 +317,17 @@ def connect(irc):
# Now, map all the ABCD type modes: # Now, map all the ABCD type modes:
'*A': 'beIq', '*B': 'k', '*C': 'l', '*D': 'mnprst'} '*A': 'beIq', '*B': 'k', '*C': 'l', '*D': 'mnprst'}
if irc.serverdata.get('use_owner'): if self.irc.serverdata.get('use_owner'):
chary_cmodes['owner'] = 'y' chary_cmodes['owner'] = 'y'
irc.prefixmodes['y'] = '~' self.irc.prefixmodes['y'] = '~'
if irc.serverdata.get('use_admin'): if self.irc.serverdata.get('use_admin'):
chary_cmodes['admin'] = 'a' chary_cmodes['admin'] = 'a'
irc.prefixmodes['a'] = '!' self.irc.prefixmodes['a'] = '!'
if irc.serverdata.get('use_halfop'): if self.irc.serverdata.get('use_halfop'):
chary_cmodes['halfop'] = 'h' chary_cmodes['halfop'] = 'h'
irc.prefixmodes['h'] = '%' self.irc.prefixmodes['h'] = '%'
irc.cmodes.update(chary_cmodes) self.irc.cmodes.update(chary_cmodes)
# Same thing with umodes: # Same thing with umodes:
# bot callerid cloak deaf_commonchan helpop hidechans hideoper invisible oper regdeaf servprotect showwhois snomask u_registered u_stripcolor wallops # bot callerid cloak deaf_commonchan helpop hidechans hideoper invisible oper regdeaf servprotect showwhois snomask u_registered u_stripcolor wallops
@ -304,24 +338,24 @@ def connect(irc):
'l', 'l',
# Now, map all the ABCD type modes: # Now, map all the ABCD type modes:
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzl'} '*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzl'}
irc.umodes.update(chary_umodes) self.irc.umodes.update(chary_umodes)
# Toggles support of shadowircd/elemental-ircd specific channel modes: # Toggles support of shadowircd/elemental-ircd specific channel modes:
# +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin), # +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin),
# +K (no repeat messages), +d (no nick changes), and user modes: # +K (no repeat messages), +d (no nick changes), and user modes:
# +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides channel list) # +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides channel list)
if irc.serverdata.get('use_elemental_modes'): if self.irc.serverdata.get('use_elemental_modes'):
elemental_cmodes = {'nonotice': 'T', 'hiddenbans': 'u', 'nokick': 'E', elemental_cmodes = {'nonotice': 'T', 'hiddenbans': 'u', 'nokick': 'E',
'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd'} 'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd'}
irc.cmodes.update(elemental_cmodes) self.irc.cmodes.update(elemental_cmodes)
irc.cmodes['*D'] += ''.join(elemental_cmodes.values()) self.irc.cmodes['*D'] += ''.join(elemental_cmodes.values())
elemental_umodes = {'u_noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'u_noinvite': 'V', elemental_umodes = {'u_noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'u_noinvite': 'V',
'hidechans': 'I'} 'hidechans': 'I'}
irc.umodes.update(elemental_umodes) self.irc.umodes.update(elemental_umodes)
irc.umodes['*D'] += ''.join(elemental_umodes.values()) self.irc.umodes['*D'] += ''.join(elemental_umodes.values())
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55
f('PASS %s TS 6 %s' % (irc.serverdata["sendpass"], irc.sid)) f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid))
# We request the following capabilities (for charybdis): # We request the following capabilities (for charybdis):
@ -342,10 +376,93 @@ def connect(irc):
# and allows sending CHGHOST without ENCAP. # and allows sending CHGHOST without ENCAP.
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID') f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID')
f('SERVER %s 0 :%s' % (irc.serverdata["hostname"], f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"],
irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']))
def handle_ping(irc, source, command, args): def handle_events(self, data):
"""Generic event handler for the TS6 protocol: does protocol negotation
and passes commands to handle_ABCD() functions elsewhere in this module."""
# TS6 messages:
# :42X COMMAND arg1 arg2 :final long arg
# :42XAAAAAA PRIVMSG #somewhere :hello!
args = data.split(" ")
if not args:
# No data??
return
if args[0] == 'PASS':
# <- PASS $somepassword TS 6 :42X
if args[1] != self.irc.serverdata['recvpass']:
# Check if recvpass is correct
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
if 'TS 6' not in data:
raise ProtocolError("Remote protocol version is too old! Is this even TS6?")
# Server name and SID are sent in different messages, grr
numeric = data.rsplit(':', 1)[1]
log.debug('(%s) Found uplink SID as %r', self.irc.name, numeric)
self.irc.servers[numeric] = IrcServer(None, 'unknown')
self.irc.uplink = numeric
return
elif args[0] == 'SERVER':
# <- SERVER charybdis.midnight.vpn 1 :charybdis test server
sname = args[1].lower()
log.debug('(%s) Found uplink server name as %r', self.irc.name, sname)
self.irc.servers[self.irc.uplink].name = sname
# According to the TS6 protocol documentation, we should send SVINFO
# when we get our uplink's SERVER command.
self.irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
elif args[0] == 'SQUIT':
# What? Charybdis send this in a different format!
# <- SQUIT 00A :Remote host closed the connection
split_server = args[1]
res = self.handle_squit(split_server, 'SQUIT', [split_server])
self.irc.callHooks([split_server, 'SQUIT', res])
elif args[0] == 'CAPAB':
# We only get a list of keywords here. Charybdis obviously assumes that
# we know what modes it supports (indeed, this is a standard list).
# <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN
self.irc.caps = caps = data.split(':', 1)[1].split()
for required_cap in ('EUID', 'SAVE', 'TB', 'ENCAP', 'QS'):
if required_cap not in caps:
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
if 'EX' in caps:
self.irc.cmodes['banexception'] = 'e'
if 'IE' in caps:
self.irc.cmodes['invex'] = 'I'
if 'SERVICES' in caps:
self.irc.cmodes['regonly'] = 'r'
log.debug('(%s) self.irc.connected set!', self.irc.name)
self.irc.connected.set()
# Charybdis doesn't have the idea of an explicit endburst; but some plugins
# like relay require it to know that the network's connected.
# We'll set a timer to manually call endburst. It's not beautiful,
# but it's the best we can do.
endburst_timer = threading.Timer(1, self.irc.callHooks, args=([self.irc.uplink, 'ENDBURST', {}],))
log.debug('(%s) Starting delay to send ENDBURST', self.irc.name)
endburst_timer.start()
try:
args = self.parseTS6Args(args)
numeric = args[0]
command = args[1]
args = args[2:]
except IndexError:
return
# We will do wildcard command handling here. Unhandled commands are just ignored.
try:
func = getattr(self, 'handle_'+command.lower())
except AttributeError: # Unhandled command
pass
else:
parsed_args = func(numeric, command, args)
if parsed_args is not None:
return [numeric, command, parsed_args]
def handle_ping(self, source, command, args):
"""Handles incoming PING commands."""
# PING: # PING:
# source: any # source: any
# parameters: origin, opt. destination server # parameters: origin, opt. destination server
@ -359,33 +476,35 @@ def handle_ping(irc, source, command, args):
try: try:
destination = args[1] destination = args[1]
except IndexError: except IndexError:
destination = irc.sid destination = self.irc.sid
if utils.isInternalServer(irc, destination): if utils.isInternalServer(self.irc, destination):
_send(irc, destination, 'PONG %s %s' % (destination, source)) self._send(destination, 'PONG %s %s' % (destination, source))
def handle_pong(irc, source, command, args): def handle_pong(self, source, command, args):
if source == irc.uplink: """Handles incoming PONG commands."""
irc.lastping = time.time() if source == self.irc.uplink:
self.irc.lastping = time.time()
def handle_sjoin(irc, servernumeric, command, args): def handle_sjoin(self, servernumeric, command, args):
"""Handles incoming SJOIN commands."""
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
channel = utils.toLower(irc, args[1]) channel = utils.toLower(self.irc, args[1])
userlist = args[-1].split() userlist = args[-1].split()
our_ts = irc.channels[channel].ts our_ts = self.irc.channels[channel].ts
their_ts = int(args[0]) their_ts = int(args[0])
if their_ts < our_ts: if their_ts < our_ts:
# Channel timestamp was reset on burst # Channel timestamp was reset on burst
log.debug('(%s) Setting channel TS of %s to %s from %s', log.debug('(%s) Setting channel TS of %s to %s from %s',
irc.name, channel, their_ts, our_ts) self.irc.name, channel, their_ts, our_ts)
irc.channels[channel].ts = their_ts self.irc.channels[channel].ts = their_ts
irc.channels[channel].modes.clear() self.irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values(): for p in self.irc.channels[channel].prefixmodes.values():
p.clear() p.clear()
modestring = args[2:-1] or args[2] modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(irc, channel, modestring) parsedmodes = utils.parseModes(self.irc, channel, modestring)
utils.applyModes(irc, channel, parsedmodes) utils.applyModes(self.irc, channel, parsedmodes)
namelist = [] namelist = []
log.debug('(%s) handle_sjoin: got userlist %r for %r', irc.name, userlist, channel) log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
for userpair in userlist: for userpair in userlist:
# charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4" # charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4"
r = re.search(r'([^\d]*)(.*)', userpair) r = re.search(r'([^\d]*)(.*)', userpair)
@ -393,48 +512,50 @@ def handle_sjoin(irc, servernumeric, command, args):
modeprefix = r.group(1) or '' modeprefix = r.group(1) or ''
finalprefix = '' finalprefix = ''
assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', irc.name, modeprefix, user) log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user)
for m in modeprefix: for m in modeprefix:
# Iterate over the mapping of prefix chars to prefixes, and # Iterate over the mapping of prefix chars to prefixes, and
# find the characters that match. # find the characters that match.
for char, prefix in irc.prefixmodes.items(): for char, prefix in self.irc.prefixmodes.items():
if m == prefix: if m == prefix:
finalprefix += char finalprefix += char
namelist.append(user) namelist.append(user)
irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
if their_ts <= our_ts: if their_ts <= our_ts:
utils.applyModes(irc, channel, [('+%s' % mode, user) for mode in finalprefix]) utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in finalprefix])
irc.channels[channel].users.add(user) self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
def handle_join(irc, numeric, command, args): def handle_join(self, numeric, command, args):
"""Handles incoming channel JOINs."""
# parameters: channelTS, channel, '+' (a plus sign) # parameters: channelTS, channel, '+' (a plus sign)
ts = int(args[0]) ts = int(args[0])
if args[0] == '0': if args[0] == '0':
# /join 0; part the user from all channels # /join 0; part the user from all channels
oldchans = irc.users[numeric].channels.copy() oldchans = self.irc.users[numeric].channels.copy()
log.debug('(%s) Got /join 0 from %r, channel list is %r', log.debug('(%s) Got /join 0 from %r, channel list is %r',
irc.name, numeric, oldchans) self.irc.name, numeric, oldchans)
for channel in oldchans: for channel in oldchans:
irc.channels[channel].users.discard(numeric) self.irc.channels[channel].users.discard(numeric)
irc.users[numeric].channels.discard(channel) self.irc.users[numeric].channels.discard(channel)
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
else: else:
channel = utils.toLower(irc, args[1]) channel = utils.toLower(self.irc, args[1])
our_ts = irc.channels[channel].ts our_ts = self.irc.channels[channel].ts
if ts < our_ts: if ts < our_ts:
# Channel timestamp was reset on burst # Channel timestamp was reset on burst
log.debug('(%s) Setting channel TS of %s to %s from %s', log.debug('(%s) Setting channel TS of %s to %s from %s',
irc.name, channel, ts, our_ts) self.irc.name, channel, ts, our_ts)
irc.channels[channel].ts = ts self.irc.channels[channel].ts = ts
irc.channels[channel].users.add(numeric) self.irc.channels[channel].users.add(numeric)
irc.users[numeric].channels.add(channel) self.irc.users[numeric].channels.add(channel)
# We send users and modes here because SJOIN and JOIN both use one hook, # We send users and modes here because SJOIN and JOIN both use one hook,
# for simplicity's sake (with plugins). # for simplicity's sake (with plugins).
return {'channel': channel, 'users': [numeric], 'modes': return {'channel': channel, 'users': [numeric], 'modes':
irc.channels[channel].modes, 'ts': ts} self.irc.channels[channel].modes, 'ts': ts}
def handle_euid(irc, numeric, command, args): def handle_euid(self, numeric, command, args):
"""Handles incoming EUID commands (user introduction)."""
# <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname # <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
nick = args[0] nick = args[0]
ts, modes, ident, host, ip, uid, realhost = args[2:9] ts, modes, ident, host, ip, uid, realhost = args[2:9]
@ -442,24 +563,26 @@ def handle_euid(irc, numeric, command, args):
realhost = None realhost = None
realname = args[-1] realname = args[-1]
log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s ' log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s '
'host=%s realname=%s realhost=%s ip=%s', irc.name, nick, ts, uid, 'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid,
ident, host, realname, realhost, ip) ident, host, realname, realhost, ip)
irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(irc, uid, [modes]) parsedmodes = utils.parseModes(self.irc, uid, [modes])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes) utils.applyModes(self.irc, uid, parsedmodes)
irc.servers[numeric].users.add(uid) self.irc.servers[numeric].users.add(uid)
# Call the OPERED UP hook if +o is in the mode list.
if ('o', None) in parsedmodes: if ('o', None) in parsedmodes:
otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator' otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator'
irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}]) self.irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}])
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_uid(irc, numeric, command, args): def handle_uid(self, numeric, command, args):
raise ProtocolError("Servers should use EUID instead of UID to send users! " raise ProtocolError("Servers should use EUID instead of UID to send users! "
"This IS a required capability after all...") "This IS a required capability after all...")
def handle_server(irc, numeric, command, args): def handle_server(self, numeric, command, args):
"""Handles incoming SERVER introductions."""
# parameters: server name, hopcount, sid, server description # parameters: server name, hopcount, sid, server description
servername = args[0].lower() servername = args[0].lower()
try: try:
@ -470,149 +593,47 @@ def handle_server(irc, numeric, command, args):
# XXX: don't just save these by their server names; that's ugly! # XXX: don't just save these by their server names; that's ugly!
sid = servername sid = servername
sdesc = args[-1] sdesc = args[-1]
irc.servers[sid] = IrcServer(numeric, servername) self.irc.servers[sid] = IrcServer(numeric, servername)
return {'name': servername, 'sid': sid, 'text': sdesc} return {'name': servername, 'sid': sid, 'text': sdesc}
handle_sid = handle_server handle_sid = handle_server
def handle_tmode(irc, numeric, command, args): def handle_tmode(self, numeric, command, args):
"""Handles incoming TMODE commands (channel mode change)."""
# <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4
channel = utils.toLower(irc, args[1]) channel = utils.toLower(self.irc, args[1])
modes = args[2:] modes = args[2:]
changedmodes = utils.parseModes(irc, channel, modes) changedmodes = utils.parseModes(self.irc, channel, modes)
utils.applyModes(irc, channel, changedmodes) utils.applyModes(self.irc, channel, changedmodes)
ts = int(args[0]) ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts} return {'target': channel, 'modes': changedmodes, 'ts': ts}
def handle_mode(irc, numeric, command, args): def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes."""
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc # <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0] target = args[0]
modestrings = args[1:] modestrings = args[1:]
changedmodes = utils.parseModes(irc, numeric, modestrings) changedmodes = utils.parseModes(self.irc, numeric, modestrings)
utils.applyModes(irc, target, changedmodes) utils.applyModes(self.irc, target, changedmodes)
# Call the OPERED UP hook if +o is being set.
if ('+o', None) in changedmodes: if ('+o', None) in changedmodes:
otype = 'Server_Administrator' if ('a', None) in irc.users[target].modes else 'IRC_Operator' otype = 'Server_Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC_Operator'
irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}]) self.irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}])
return {'target': target, 'modes': changedmodes} return {'target': target, 'modes': changedmodes}
def handle_events(irc, data): def handle_tb(self, numeric, command, args):
# TS6 messages: """Handles incoming topic burst (TB) commands."""
# :42X COMMAND arg1 arg2 :final long arg
# :42XAAAAAA PRIVMSG #somewhere :hello!
args = data.split(" ")
if not args:
# No data??
return
if args[0] == 'PASS':
# <- PASS $somepassword TS 6 :42X
if args[1] != irc.serverdata['recvpass']:
# Check if recvpass is correct
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
if 'TS 6' not in data:
raise ProtocolError("Remote protocol version is too old! Is this even TS6?")
# Server name and SID are sent in different messages, grr
numeric = data.rsplit(':', 1)[1]
log.debug('(%s) Found uplink SID as %r', irc.name, numeric)
irc.servers[numeric] = IrcServer(None, 'unknown')
irc.uplink = numeric
return
elif args[0] == 'SERVER':
# <- SERVER charybdis.midnight.vpn 1 :charybdis test server
sname = args[1].lower()
log.debug('(%s) Found uplink server name as %r', irc.name, sname)
irc.servers[irc.uplink].name = sname
# According to the TS6 protocol documentation, we should send SVINFO
# when we get our uplink's SERVER command.
irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
elif args[0] == 'SQUIT':
# What? Charybdis send this in a different format!
# <- SQUIT 00A :Remote host closed the connection
split_server = args[1]
res = handle_squit(irc, split_server, 'SQUIT', [split_server])
irc.callHooks([split_server, 'SQUIT', res])
elif args[0] == 'CAPAB':
# We only get a list of keywords here. Charybdis obviously assumes that
# we know what modes it supports (indeed, this is a standard list).
# <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN
irc.caps = caps = data.split(':', 1)[1].split()
for required_cap in ('EUID', 'SAVE', 'TB', 'ENCAP', 'QS'):
if required_cap not in caps:
raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps))
if 'EX' in caps:
irc.cmodes['banexception'] = 'e'
if 'IE' in caps:
irc.cmodes['invex'] = 'I'
if 'SERVICES' in caps:
irc.cmodes['regonly'] = 'r'
log.debug('(%s) irc.connected set!', irc.name)
irc.connected.set()
# Charybdis doesn't have the idea of an explicit endburst; but some plugins
# like relay require it to know that the network's connected.
# We'll set a timer to manually call endburst. It's not beautiful,
# but it's the best we can do.
endburst_timer = threading.Timer(1, irc.callHooks, args=([irc.uplink, 'ENDBURST', {}],))
log.debug('(%s) Starting delay to send ENDBURST', irc.name)
endburst_timer.start()
try:
args = parseTS6Args(args)
numeric = args[0]
command = args[1]
args = args[2:]
except IndexError:
return
# We will do wildcard event handling here. Unhandled events are just ignored.
try:
func = globals()['handle_'+command.lower()]
except KeyError: # unhandled event
pass
else:
parsed_args = func(irc, numeric, command, args)
if parsed_args is not None:
return [numeric, command, parsed_args]
def spawnServer(irc, name, sid=None, uplink=None, desc=None):
# -> :0AL SID test.server 1 0XY :some silly pseudoserver
uplink = uplink or irc.sid
name = name.lower()
desc = desc or irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']
if sid is None: # No sid given; generate one!
irc.sidgen = utils.TS6SIDGenerator(irc.serverdata["sidrange"])
sid = irc.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
for server in irc.servers.values():
if name == server.name:
raise ValueError('A server named %r already exists!' % name)
if not utils.isInternalServer(irc, uplink):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink)
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
_send(irc, uplink, 'SID %s 1 %s :%s' % (name, sid, desc))
irc.servers[sid] = IrcServer(uplink, name, internal=True)
return sid
def squitServer(irc, source, target, text='No reason given'):
# -> SQUIT 9PZ :blah, blah
irc.send('SQUIT %s :%s' % (target, text))
handle_squit(irc, source, 'SQUIT', [target, text])
def handle_tb(irc, numeric, command, args):
# <- :42X TB 1434510754 #channel GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic # <- :42X TB 1434510754 #channel GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic
channel = args[1].lower() channel = args[1].lower()
ts = args[0] ts = args[0]
setter = args[2] setter = args[2]
topic = args[-1] topic = args[-1]
irc.channels[channel].topic = topic self.irc.channels[channel].topic = topic
irc.channels[channel].topicset = True self.irc.channels[channel].topicset = True
return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic}
def handle_invite(irc, numeric, command, args): def handle_invite(self, numeric, command, args):
"""Handles incoming INVITEs."""
# <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345 # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345
target = args[0] target = args[0]
channel = args[1].lower() channel = args[1].lower()
@ -623,12 +644,14 @@ def handle_invite(irc, numeric, command, args):
# We don't actually need to process this; it's just something plugins/hooks can use # We don't actually need to process this; it's just something plugins/hooks can use
return {'target': target, 'channel': channel} return {'target': target, 'channel': channel}
def handle_chghost(irc, numeric, command, args): def handle_chghost(self, numeric, command, args):
"""Handles incoming CHGHOST commands."""
target = args[0] target = args[0]
irc.users[target].host = newhost = args[1] self.irc.users[target].host = newhost = args[1]
return {'target': numeric, 'newhost': newhost} return {'target': numeric, 'newhost': newhost}
def handle_bmask(irc, numeric, command, args): def handle_bmask(self, numeric, command, args):
"""Handles incoming BMASK commands (ban propagation on burst)."""
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
# This is used for propagating bans, not TMODE! # This is used for propagating bans, not TMODE!
channel = args[1].lower() channel = args[1].lower()
@ -637,18 +660,26 @@ def handle_bmask(irc, numeric, command, args):
modes = [] modes = []
for ban in args[-1].split(): for ban in args[-1].split():
modes.append(('+%s' % mode, ban)) modes.append(('+%s' % mode, ban))
utils.applyModes(irc, channel, modes) utils.applyModes(self.irc, channel, modes)
return {'target': channel, 'modes': modes, 'ts': ts} return {'target': channel, 'modes': modes, 'ts': ts}
def handle_whois(irc, numeric, command, args): def handle_whois(self, numeric, command, args):
"""Handles incoming WHOIS commands.
Note: The core of WHOIS handling is done by coreplugin.py
(IRCd-independent), and not here."""
# <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel # <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel
return {'target': args[0]} return {'target': args[0]}
def handle_472(irc, numeric, command, args): def handle_472(self, numeric, command, args):
"""Handles the incoming 472 numeric.
472 is sent to us when one of our clients tries to set a mode the uplink
server doesn't support. In this case, we'll raise a warning to alert
the administrator that certain extensions should be loaded for the best
compatibility.
"""
# <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me # <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me
# 472 is sent to us when one of our clients tries to set a mode the server
# doesn't support. In this case, we'll raise a warning to alert the user
# about it.
badmode = args[1] badmode = args[1]
reason = args[-1] reason = args[-1]
setter = args[0] setter = args[0]
@ -657,15 +688,16 @@ def handle_472(irc, numeric, command, args):
log.warning('(%s) User %r attempted to set channel mode %r, but the ' log.warning('(%s) User %r attempted to set channel mode %r, but the '
'extension providing it isn\'t loaded! To prevent possible' 'extension providing it isn\'t loaded! To prevent possible'
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to ' ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
'your IRCd configuration.', irc.name, setter, badmode, 'your IRCd configuration.', self.irc.name, setter, badmode,
charlist[badmode]) charlist[badmode])
def handle_away(irc, numeric, command, args): def handle_away(self, numeric, command, args):
"""Handles incoming AWAY messages."""
# <- :6ELAAAAAB AWAY :Auto-away # <- :6ELAAAAAB AWAY :Auto-away
try: try:
irc.users[numeric].away = text = args[0] self.irc.users[numeric].away = text = args[0]
except IndexError: # User is unsetting away status except IndexError: # User is unsetting away status
irc.users[numeric].away = text = '' self.irc.users[numeric].away = text = ''
return {'text': text} return {'text': text}
Class = TS6Protocol

View File

@ -7,12 +7,13 @@ import utils
from log import log from log import log
from classes import * from classes import *
def _send(irc, sid, msg): class TS6BaseProtocol(Protocol):
irc.send(':%s %s' % (sid, msg)) def _send(self, source, msg):
"""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(args): def parseArgs(self, args):
"""<arg list> """Parses a string of RFC1459-style arguments split into a list, where ":" may
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. be used for multi-word arguments that last until the end of a line.
""" """
real_args = [] real_args = []
@ -34,218 +35,212 @@ def parseArgs(args):
break break
return real_args return real_args
def parseTS6Args(args): def parseTS6Args(self, args):
"""<arg list> """Similar to parseArgs(), but stripping leading colons from the first argument
Similar to parseArgs(), but stripping leading colons from the first argument
of a line (usually the sender field).""" of a line (usually the sender field)."""
args = parseArgs(args) args = self.parseArgs(args)
args[0] = args[0].split(':', 1)[1] args[0] = args[0].split(':', 1)[1]
return args return args
### OUTGOING COMMANDS ### OUTGOING COMMANDS
def _sendKick(irc, numeric, channel, target, reason=None): def _sendKick(self, numeric, channel, target, reason=None):
"""<irc object> <kicker client numeric> """Internal function to send kicks from a PyLink client/server."""
channel = utils.toLower(self.irc, channel)
Sends a kick from a PyLink PseudoClient."""
channel = utils.toLower(irc, channel)
if not reason: if not reason:
reason = 'No reason given' reason = 'No reason given'
_send(irc, numeric, 'KICK %s %s :%s' % (channel, target, reason)) self._send(numeric, 'KICK %s %s :%s' % (channel, target, reason))
# We can pretend the target left by its own will; all we really care about # We can pretend the target left by its own will; all we really care about
# is that the target gets removed from the channel userlist, and calling # is that the target gets removed from the channel userlist, and calling
# handle_part() does that just fine. # handle_part() does that just fine.
handle_part(irc, target, 'KICK', [channel]) self.handle_part(target, 'KICK', [channel])
def kickClient(irc, numeric, channel, target, reason=None): def kickClient(self, numeric, channel, target, reason=None):
if not utils.isInternalClient(irc, numeric): """Sends a kick from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_sendKick(irc, numeric, channel, target, reason=reason) self._sendKick(numeric, channel, target, reason=reason)
def kickServer(irc, numeric, channel, target, reason=None): def kickServer(self, numeric, channel, target, reason=None):
if not utils.isInternalServer(irc, numeric): """Sends a kick from a PyLink server."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.') raise LookupError('No such PyLink PseudoServer exists.')
_sendKick(irc, numeric, channel, target, reason=reason) self._sendKick(numeric, channel, target, reason=reason)
def nickClient(irc, numeric, newnick): def nickClient(self, numeric, newnick):
"""<irc object> <client numeric> <new nickname> """Changes the nick of a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Changes the nick of a PyLink PseudoClient."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'NICK %s %s' % (newnick, int(time.time()))) self._send(numeric, 'NICK %s %s' % (newnick, int(time.time())))
irc.users[numeric].nick = newnick self.irc.users[numeric].nick = newnick
def removeClient(irc, numeric): def removeClient(self, numeric):
"""<irc object> <client numeric> """Internal function to remove a client from our internal state."""
for c, v in self.irc.channels.copy().items():
Removes a client from our internal databases, regardless
of whether it's one of our pseudoclients or not."""
for c, v in irc.channels.copy().items():
v.removeuser(numeric) v.removeuser(numeric)
# Clear empty non-permanent channels. # Clear empty non-permanent channels.
if not (irc.channels[c].users or ((irc.cmodes.get('permanent'), None) in irc.channels[c].modes)): if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)):
del irc.channels[c] del self.irc.channels[c]
sid = numeric[:3] sid = numeric[:3]
log.debug('Removing client %s from irc.users', numeric) log.debug('Removing client %s from self.irc.users', numeric)
del irc.users[numeric] del self.irc.users[numeric]
log.debug('Removing client %s from irc.servers[%s]', numeric, sid) log.debug('Removing client %s from self.irc.servers[%s]', numeric, sid)
irc.servers[sid].users.discard(numeric) self.irc.servers[sid].users.discard(numeric)
def partClient(irc, client, channel, reason=None): def partClient(self, client, channel, reason=None):
channel = utils.toLower(irc, channel) """Sends a part from a PyLink client."""
if not utils.isInternalClient(irc, client): channel = utils.toLower(self.irc, channel)
log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', irc.name, client, channel) if not utils.isInternalClient(self.irc, client):
log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel)
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
msg = "PART %s" % channel msg = "PART %s" % channel
if reason: if reason:
msg += " :%s" % reason msg += " :%s" % reason
_send(irc, client, msg) self._send(client, msg)
handle_part(irc, client, 'PART', [channel]) self.handle_part(client, 'PART', [channel])
def quitClient(irc, numeric, reason): def quitClient(self, numeric, reason):
"""<irc object> <client numeric> """Quits a PyLink client."""
if utils.isInternalClient(self.irc, numeric):
Quits a PyLink PseudoClient.""" self._send(numeric, "QUIT :%s" % reason)
if utils.isInternalClient(irc, numeric): self.removeClient(numeric)
_send(irc, numeric, "QUIT :%s" % reason)
removeClient(irc, numeric)
else: else:
raise LookupError("No such PyLink PseudoClient exists.") raise LookupError("No such PyLink PseudoClient exists.")
def messageClient(irc, numeric, target, text): def messageClient(self, numeric, target, text):
"""<irc object> <client numeric> <text> """Sends a PRIVMSG from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Sends PRIVMSG <text> from PyLink client <client numeric>."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'PRIVMSG %s :%s' % (target, text)) self._send(numeric, 'PRIVMSG %s :%s' % (target, text))
def noticeClient(irc, numeric, target, text): def noticeClient(self, numeric, target, text):
"""<irc object> <client numeric> <text> """Sends a NOTICE from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
Sends NOTICE <text> from PyLink client <client numeric>."""
if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'NOTICE %s :%s' % (target, text)) self._send(numeric, 'NOTICE %s :%s' % (target, text))
def topicClient(irc, numeric, target, text): def topicClient(self, numeric, target, text):
if not utils.isInternalClient(irc, numeric): """Sends a TOPIC change from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'TOPIC %s :%s' % (target, text)) self._send(numeric, 'TOPIC %s :%s' % (target, text))
irc.channels[target].topic = text self.irc.channels[target].topic = text
irc.channels[target].topicset = True self.irc.channels[target].topicset = True
### HANDLERS ### HANDLERS
def handle_privmsg(irc, source, command, args): def handle_privmsg(self, source, command, args):
"""Handles incoming PRIVMSG/NOTICE."""
# <- :70MAAAAAA PRIVMSG #dev :afasfsa # <- :70MAAAAAA PRIVMSG #dev :afasfsa
# <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa
target = args[0] target = args[0]
# We use lowercase channels internally, but uppercase UIDs. # We use lowercase channels internally, but uppercase UIDs.
if utils.isChannel(target): if utils.isChannel(target):
target = utils.toLower(irc, target) target = utils.toLower(self.irc, target)
return {'target': target, 'text': args[1]} return {'target': target, 'text': args[1]}
handle_notice = handle_privmsg handle_notice = handle_privmsg
def handle_kill(irc, source, command, args): def handle_kill(self, source, command, args):
"""Handles incoming KILLs."""
killed = args[0] killed = args[0]
data = irc.users.get(killed) # Depending on whether the self.ircd sends explicit QUIT messages for
# KILLed clients, the user may or may not have automatically been removed.
# If not, we have to assume that KILL = QUIT and remove them ourselves.
data = self.irc.users.get(killed)
if data: if data:
removeClient(irc, killed) self.removeClient(killed)
return {'target': killed, 'text': args[1], 'userdata': data} return {'target': killed, 'text': args[1], 'userdata': data}
def handle_kick(irc, source, command, args): def handle_kick(self, source, command, args):
"""Handles incoming KICKs."""
# :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason
channel = utils.toLower(irc, args[0]) channel = utils.toLower(self.irc, args[0])
kicked = args[1] kicked = args[1]
handle_part(irc, kicked, 'KICK', [channel, args[2]]) self.handle_part(kicked, 'KICK', [channel, args[2]])
return {'channel': channel, 'target': kicked, 'text': args[2]} return {'channel': channel, 'target': kicked, 'text': args[2]}
def handle_error(irc, numeric, command, args): def handle_error(self, numeric, command, args):
irc.connected.clear() """Handles ERROR messages - these mean that our uplink has disconnected us!"""
raise ProtocolError('Received an ERROR, killing!') self.irc.connected.clear()
raise ProtocolError('Received an ERROR, disconnecting!')
def handle_nick(irc, numeric, command, args): def handle_nick(self, numeric, command, args):
"""Handles incoming NICK changes."""
# <- :70MAAAAAA NICK GL-devel 1434744242 # <- :70MAAAAAA NICK GL-devel 1434744242
oldnick = irc.users[numeric].nick oldnick = self.irc.users[numeric].nick
newnick = irc.users[numeric].nick = args[0] newnick = self.irc.users[numeric].nick = args[0]
return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])} return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])}
def handle_quit(irc, numeric, command, args): def handle_quit(self, numeric, command, args):
"""Handles incoming QUIT commands."""
# <- :1SRAAGB4T QUIT :Quit: quit message goes here # <- :1SRAAGB4T QUIT :Quit: quit message goes here
removeClient(irc, numeric) self.removeClient(numeric)
return {'text': args[0]} return {'text': args[0]}
def handle_save(irc, numeric, command, args): def handle_save(self, numeric, command, args):
# This is used to handle nick collisions. Here, the client Derp_ already exists, """Handles incoming SAVE messages, used to handle nick collisions."""
# so trying to change nick to it will cause a nick collision. On InspIRCd, # In this below example, the client Derp_ already exists,
# this will simply set the collided user's nick to its UID. # and trying to change someone's nick to it will cause a nick
# collision. On TS6 self.ircds, this will simply set the collided user's
# nick to its UID.
# <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_ # <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_
# -> :0AL000001 NICK Derp_ 1433728673 # -> :0AL000001 NICK Derp_ 1433728673
# <- :70M SAVE 0AL000001 1433728673 # <- :70M SAVE 0AL000001 1433728673
user = args[0] user = args[0]
oldnick = irc.users[user].nick oldnick = self.irc.users[user].nick
irc.users[user].nick = user self.irc.users[user].nick = user
return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick} return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick}
def handle_squit(irc, numeric, command, args): def handle_squit(self, numeric, command, args):
"""Handles incoming SQUITs (netsplits)."""
# :70M SQUIT 1ML :Server quit by GL!gl@0::1 # :70M SQUIT 1ML :Server quit by GL!gl@0::1
split_server = args[0] split_server = args[0]
affected_users = [] affected_users = []
log.info('(%s) Netsplit on server %s', irc.name, split_server) log.info('(%s) Netsplit on server %s', self.irc.name, split_server)
# Prevent RuntimeError: dictionary changed size during iteration # Prevent RuntimeError: dictionary changed size during iteration
old_servers = irc.servers.copy() old_servers = self.irc.servers.copy()
for sid, data in old_servers.items(): for sid, data in old_servers.items():
if data.uplink == split_server: if data.uplink == split_server:
log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid) log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid)
args = handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid]) args = self.handle_squit(sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid])
affected_users += args['users'] affected_users += args['users']
for user in irc.servers[split_server].users.copy(): for user in self.irc.servers[split_server].users.copy():
affected_users.append(user) affected_users.append(user)
log.debug('Removing client %s (%s)', user, irc.users[user].nick) log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
removeClient(irc, user) self.removeClient(user)
del irc.servers[split_server] del self.irc.servers[split_server]
log.debug('(%s) Netsplit affected users: %s', irc.name, affected_users) log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)
return {'target': split_server, 'users': affected_users} return {'target': split_server, 'users': affected_users}
def handle_mode(irc, numeric, command, args): def handle_topic(self, numeric, command, args):
# In InspIRCd, MODE is used for setting user modes and """Handles incoming TOPIC changes from clients. For topic bursts,
# FMODE is used for channel modes: TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead."""
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0]
modestrings = args[1:]
changedmodes = utils.parseModes(irc, numeric, modestrings)
utils.applyModes(irc, target, changedmodes)
return {'target': target, 'modes': changedmodes}
def handle_topic(irc, numeric, command, args):
# <- :70MAAAAAA TOPIC #test :test # <- :70MAAAAAA TOPIC #test :test
channel = utils.toLower(irc, args[0]) channel = utils.toLower(self.irc, args[0])
topic = args[1] topic = args[1]
ts = int(time.time()) ts = int(time.time())
irc.channels[channel].topic = topic self.irc.channels[channel].topic = topic
irc.channels[channel].topicset = True self.irc.channels[channel].topicset = True
return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic}
def handle_part(irc, source, command, args): def handle_part(self, source, command, args):
channels = utils.toLower(irc, args[0]).split(',') """Handles incoming PART commands."""
channels = utils.toLower(self.irc, args[0]).split(',')
for channel in channels: for channel in channels:
# We should only get PART commands for channels that exist, right?? # We should only get PART commands for channels that exist, right??
irc.channels[channel].removeuser(source) self.irc.channels[channel].removeuser(source)
try: try:
irc.users[source].channels.discard(channel) self.irc.users[source].channels.discard(channel)
except KeyError: except KeyError:
log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", irc.name, channel, source) log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.irc.name, channel, source)
try: try:
reason = args[1] reason = args[1]
except IndexError: except IndexError:
reason = '' reason = ''
# Clear empty non-permanent channels. # Clear empty non-permanent channels.
if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)): if not (self.irc.channels[channel].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[channel].modes)):
del irc.channels[channel] del self.irc.channels[channel]
return {'channels': channels, 'text': reason} return {'channels': channels, 'text': reason}

View File

@ -10,7 +10,7 @@ cd "$WRAPPER_DIR"
if [[ ! -z "$(which cpulimit)" ]]; then if [[ ! -z "$(which cpulimit)" ]]; then
# -z makes cpulimit exit when PyLink dies. # -z makes cpulimit exit when PyLink dies.
cpulimit -l $LIMIT -z ./main.py 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 "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" echo "To kill the process manually, run ./kill.sh"
else else

View File

@ -29,7 +29,7 @@ class CorePluginTestCase(tests_common.PluginTestCase):
self.assertNotEqual(self.irc.pseudoclient.uid, spmain[0]['olduser']) self.assertNotEqual(self.irc.pseudoclient.uid, spmain[0]['olduser'])
def testKickrejoin(self): def testKickrejoin(self):
self.proto.kickClient(self.irc, self.u, '#pylink', self.u, 'test') self.proto.kickClient(self.u, '#pylink', self.u, 'test')
msgs = self.irc.takeMsgs() msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs) commands = self.irc.takeCommands(msgs)
self.assertIn('FJOIN', commands) self.assertIn('FJOIN', commands)

View File

@ -7,7 +7,7 @@ import unittest
class TestFakeIRC(unittest.TestCase): class TestFakeIRC(unittest.TestCase):
def setUp(self): def setUp(self):
self.irc = classes.FakeIRC('unittest', classes.FakeProto()) self.irc = classes.FakeIRC('unittest', classes.FakeProto)
def testFakeIRC(self): def testFakeIRC(self):
self.irc.run('this should do nothing') self.irc.run('this should do nothing')

View File

@ -19,7 +19,7 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica') self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
def testConnect(self): def testConnect(self):
self.proto.connect(self.irc) self.proto.connect()
initial_messages = self.irc.takeMsgs() initial_messages = self.irc.takeMsgs()
commands = self.irc.takeCommands(initial_messages) commands = self.irc.takeCommands(initial_messages)
# SERVER pylink.unittest abcd 0 9PY :PyLink Service # SERVER pylink.unittest abcd 0 9PY :PyLink Service
@ -40,20 +40,20 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
def testHandleSQuit(self): def testHandleSQuit(self):
# Spawn a messy network map, just because! # Spawn a messy network map, just because!
self.proto.spawnServer(self.irc, 'level1.pylink', '34P') self.proto.spawnServer('level1.pylink', '34P')
self.proto.spawnServer(self.irc, 'level2.pylink', '34Q', uplink='34P') self.proto.spawnServer('level2.pylink', '34Q', uplink='34P')
self.proto.spawnServer(self.irc, 'level3.pylink', '34Z', uplink='34Q') self.proto.spawnServer('level3.pylink', '34Z', uplink='34Q')
self.proto.spawnServer(self.irc, 'level4.pylink', '34Y', uplink='34Z') self.proto.spawnServer('level4.pylink', '34Y', uplink='34Z')
self.assertEqual(self.irc.servers['34Y'].uplink, '34Z') self.assertEqual(self.irc.servers['34Y'].uplink, '34Z')
s4u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', server='34Y').uid s4u = self.proto.spawnClient('person1', 'person', 'users.overdrive.pw', server='34Y').uid
s3u = self.proto.spawnClient(self.irc, 'person2', 'person', 'users.overdrive.pw', server='34Z').uid s3u = self.proto.spawnClient('person2', 'person', 'users.overdrive.pw', server='34Z').uid
self.proto.joinClient(self.irc, s3u, '#pylink') self.proto.joinClient(s3u, '#pylink')
self.proto.joinClient(self.irc, s4u, '#pylink') self.proto.joinClient(s4u, '#pylink')
self.irc.run(':34Z SQUIT 34Y :random squit messsage') self.irc.run(':34Z SQUIT 34Y :random squit messsage')
self.assertNotIn(s4u, self.irc.users) self.assertNotIn(s4u, self.irc.users)
self.assertNotIn('34Y', self.irc.servers) self.assertNotIn('34Y', self.irc.servers)
# Netsplits are obviously recursive, so all these should be removed. # Netsplits are obviously recursive, so all these should be removed.
self.proto.handle_squit(self.irc, '9PY', 'SQUIT', ['34P']) self.proto.handle_squit('9PY', 'SQUIT', ['34P'])
self.assertNotIn(s3u, self.irc.users) self.assertNotIn(s3u, self.irc.users)
self.assertNotIn('34P', self.irc.servers) self.assertNotIn('34P', self.irc.servers)
self.assertNotIn('34Q', self.irc.servers) self.assertNotIn('34Q', self.irc.servers)
@ -136,6 +136,11 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.assertEqual(expected, hookdata) self.assertEqual(expected, hookdata)
def testHandleFMode(self): def testHandleFMode(self):
# Default channels start with +nt
self.irc.run(':70M FMODE #pylink 1423790412 -nt')
self.assertEqual(set(), self.irc.channels['#pylink'].modes)
self.irc.takeHooks()
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100') self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes) self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons') self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons')
@ -175,9 +180,10 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
def testHandleFModeRemovesOldParams(self): def testHandleFModeRemovesOldParams(self):
self.irc.run(':70M FMODE #pylink 1423790412 +l 50') self.irc.run(':70M FMODE #pylink 1423790412 +l 50')
self.assertEqual({('l', '50')}, self.irc.channels['#pylink'].modes) self.assertIn(('l', '50'), self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790412 +l 30') self.irc.run(':70M FMODE #pylink 1423790412 +l 30')
self.assertEqual({('l', '30')}, self.irc.channels['#pylink'].modes) self.assertIn(('l', '30'), self.irc.channels['#pylink'].modes)
self.assertNotIn(('l', '50'), self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks() hookdata = self.irc.takeHooks()
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}], expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}],
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]] ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]]

View File

@ -12,13 +12,13 @@ def dummyf():
class TestRelay(unittest.TestCase): class TestRelay(unittest.TestCase):
def setUp(self): def setUp(self):
self.irc = classes.FakeIRC('unittest', classes.FakeProto()) self.irc = classes.FakeIRC('fakeirc', classes.FakeProto)
self.irc.maxnicklen = 20 self.irc.maxnicklen = 20
self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick) self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick)
# Fake our protocol name to something that supports slashes in nicks. # Fake our protocol name to something that supports slashes in nicks.
# relay uses a whitelist for this to prevent accidentally introducing # relay uses a whitelist for this to prevent accidentally introducing
# bad nicks: # bad nicks:
self.irc.proto.__name__ = "inspircd" self.irc.protoname = "inspircd"
def testNormalizeNick(self): def testNormalizeNick(self):
# Second argument simply states the suffix. # Second argument simply states the suffix.
@ -36,10 +36,10 @@ class TestRelay(unittest.TestCase):
self.assertEqual(self.f('helloworld'), 'helloworl///unittest') self.assertEqual(self.f('helloworld'), 'helloworl///unittest')
def testNormalizeNickRemovesSlashes(self): def testNormalizeNickRemovesSlashes(self):
self.irc.proto.__name__ = "charybdis" self.irc.protoname = "charybdis"
try: try:
self.assertEqual(self.f('helloworld'), 'helloworld|unittest') self.assertEqual(self.f('helloworld'), 'helloworld|unittest')
self.assertEqual(self.f('abcde/eJanus'), 'abcde|eJanu|unittest') self.assertEqual(self.f('abcde/eJanus'), 'abcde|eJanu|unittest')
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously|unittest') self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
finally: finally:
self.irc.proto.__name__ = "inspircd" self.irc.protoname = "inspircd"

View File

@ -13,7 +13,7 @@ def dummyf():
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
def setUp(self): def setUp(self):
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto()) self.irc = classes.FakeIRC('fakeirc', classes.FakeProto)
def testTS6UIDGenerator(self): def testTS6UIDGenerator(self):
uidgen = utils.TS6UIDGenerator('9PY') uidgen = utils.TS6UIDGenerator('9PY')

View File

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

View File

@ -98,12 +98,6 @@ class TS6SIDGenerator():
self.increment() self.increment()
return sid return sid
def msg(irc, target, text, notice=False):
if notice:
irc.proto.noticeClient(irc, irc.pseudoclient.uid, target, text)
else:
irc.proto.messageClient(irc, irc.pseudoclient.uid, target, text)
def add_cmd(func, name=None): def add_cmd(func, name=None):
if name is None: if name is None:
name = func.__name__ name = func.__name__