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

Merge branch 'devel' into wip/document-everything

This commit is contained in:
James Lu 2015-08-20 08:14:22 -07:00
commit 834136e848
12 changed files with 396 additions and 182 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ __pycache__/
*.save* *.save*
*.db *.db
*.pid *.pid
*.pem

View File

@ -4,7 +4,7 @@ PyLink is an extensible, plugin-based IRC PseudoService written in Python. It ai
## Usage ## Usage
**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting throughout your user base! **PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting amongst your users!
That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute. That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute.
@ -17,8 +17,9 @@ Dependencies currently include:
#### Supported IRCds #### Supported IRCds
* InspIRCd 2.0.x - module: `inspircd` * InspIRCd 2.0.x - module `inspircd`
* charybdis (3.5.x / git master) - module: `ts6` * charybdis (3.5.x / git master) - module `ts6`
* Elemental-IRCd (6.6.x / git master) - module `ts6`
### Installation ### Installation

View File

@ -21,6 +21,7 @@ class IrcUser():
self.identified = False self.identified = False
self.channels = set() self.channels = set()
self.away = ''
def __repr__(self): def __repr__(self):
return repr(self.__dict__) return repr(self.__dict__)

View File

@ -53,6 +53,17 @@ servers:
# PyLink might introduce a nick that is too long and cause netsplits! # PyLink might introduce a nick that is too long and cause netsplits!
maxnicklen: 30 maxnicklen: 30
# Toggles SSL for this network. Defaults to false if not specified, and requires the
# ssl_certfile and ssl_keyfile options to work.
# ssl: true
# ssl_certfile: pylink-cert.pem
# ssl_keyfile: pylink-key.pem
# Optionally, you can set this option to verify the SSL certificate
# fingerprint (SHA1) of your uplink.
# ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
ts6net: ts6net:
ip: 127.0.0.1 ip: 127.0.0.1
port: 7000 port: 7000

View File

@ -20,7 +20,8 @@ utils.add_hook(handle_kick, 'KICK')
# Handle commands sent to the PyLink client (PRIVMSG) # Handle commands sent to the PyLink client (PRIVMSG)
def handle_commands(irc, source, command, args): def handle_commands(irc, source, command, args):
if args['target'] == irc.pseudoclient.uid: if args['target'] == irc.pseudoclient.uid:
cmd_args = args['text'].split(' ') text = args['text'].strip()
cmd_args = text.split(' ')
cmd = cmd_args[0].lower() cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:] cmd_args = cmd_args[1:]
try: try:

84
main.py
View File

@ -7,6 +7,8 @@ import time
import sys import sys
from collections import defaultdict from collections import defaultdict
import threading import threading
import ssl
import hashlib
from log import log from log import log
import conf import conf
@ -17,6 +19,9 @@ import coreplugin
class Irc(): class Irc():
def initVars(self): def initVars(self):
self.pseudoclient = None
self.connected = threading.Event()
self.lastping = time.time()
# Server, channel, and user indexes to be populated by our protocol module # Server, channel, and user indexes to be populated by our protocol module
self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)} self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)}
self.users = {} self.users = {}
@ -52,7 +57,6 @@ class Irc():
def __init__(self, netname, proto, conf): def __init__(self, netname, proto, conf):
# Initialize some variables # Initialize some variables
self.connected = threading.Event()
self.name = netname.lower() self.name = netname.lower()
self.conf = conf self.conf = conf
self.serverdata = conf['servers'][netname] self.serverdata = conf['servers'][netname]
@ -67,26 +71,78 @@ class Irc():
self.connection_thread = threading.Thread(target = self.connect) self.connection_thread = threading.Thread(target = self.connect)
self.connection_thread.start() self.connection_thread.start()
self.pingTimer = None self.pingTimer = None
self.lastping = time.time()
def connect(self): def connect(self):
ip = self.serverdata["ip"] ip = self.serverdata["ip"]
port = self.serverdata["port"] port = self.serverdata["port"]
while True: while True:
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.initVars() self.initVars()
checks_ok = True
try: try:
self.socket = socket.socket()
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after # Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional. # we've connected; this is intentional.
self.socket = socket.create_connection((ip, port), timeout=self.pingfreq) self.socket.settimeout(self.pingfreq)
self.socket.setblocking(0) self.ssl = self.serverdata.get('ssl')
if self.ssl:
log.info('(%s) Attempting SSL for this connection...', self.name)
certfile = self.serverdata.get('ssl_certfile')
keyfile = self.serverdata.get('ssl_keyfile')
if certfile and keyfile:
try:
self.socket = ssl.wrap_socket(self.socket,
certfile=certfile,
keyfile=keyfile)
except OSError:
log.exception('(%s) Caught OSError trying to '
'initialize the SSL connection; '
'are "ssl_certfile" and '
'"ssl_keyfile" set correctly?',
self.name)
checks_ok = False
else:
log.error('(%s) SSL certfile/keyfile was not set '
'correctly, aborting... ', self.name)
checks_ok = False
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.socket.connect((ip, port))
self.socket.settimeout(self.pingtimeout) self.socket.settimeout(self.pingtimeout)
if self.ssl and checks_ok:
peercert = self.socket.getpeercert(binary_form=True)
sha1fp = hashlib.sha1(peercert).hexdigest()
expected_fp = self.serverdata.get('ssl_fingerprint')
if expected_fp:
if sha1fp != expected_fp:
log.error('(%s) Uplink\'s SSL certificate '
'fingerprint (SHA1) does not match the '
'one configured: expected %r, got %r; '
'disconnecting...', self.name,
expected_fp, sha1fp)
checks_ok = False
else:
log.info('(%s) Uplink SSL certificate fingerprint '
'(SHA1) verified: %r', self.name, sha1fp)
else:
log.info('(%s) Uplink\'s SSL certificate fingerprint '
'is %r. You can enhance the security of your '
'link by specifying this in a "ssl_fingerprint"'
' option in your server block.', self.name,
sha1fp)
if checks_ok:
self.proto.connect(self) self.proto.connect(self)
self.spawnMain() self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name) log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing() self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name) log.info('(%s) Server ready; listening for data.', self.name)
self.run() self.run()
else:
log.error('(%s) A configuration error was encountered '
'trying to set up this connection. Please check'
' your configuration file and try again.',
self.name)
except (socket.error, classes.ProtocolError, ConnectionError) as e: except (socket.error, classes.ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s', log.warning('(%s) Disconnected from IRC: %s: %s',
self.name, type(e).__name__, str(e)) self.name, type(e).__name__, str(e))
@ -107,19 +163,21 @@ class Irc():
self.pingTimer.cancel() self.pingTimer.cancel()
except: # Socket timed out during creation; ignore except: # Socket timed out during creation; ignore
pass pass
# Internal hook signifying that a network has disconnected.
self.callHooks([None, 'PYLINK_DISCONNECT', {}]) self.callHooks([None, 'PYLINK_DISCONNECT', {}])
def run(self): def run(self):
buf = b"" buf = b""
data = b"" data = b""
while (time.time() - self.lastping) < self.pingtimeout: while True:
log.debug('(%s) time_since_last_ping: %s', self.name, (time.time() - self.lastping))
log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout)
data = self.socket.recv(2048) data = self.socket.recv(2048)
buf += data buf += data
if self.connected and not data: if self.connected.is_set() and not data:
log.warn('(%s) No data received and self.connected is not set; disconnecting!', self.name) log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
break return
elif (time.time() - self.lastping) > self.pingtimeout:
log.warning('(%s) Connection timed out.', self.name)
return
while b'\n' in buf: while b'\n' in buf:
line, buf = buf.split(b'\n', 1) line, buf = buf.split(b'\n', 1)
line = line.strip(b'\r') line = line.strip(b'\r')
@ -188,9 +246,13 @@ class Irc():
ident = self.botdata.get('ident') or 'pylink' ident = self.botdata.get('ident') or 'pylink'
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
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)}) self.pseudoclient = self.proto.spawnClient(self, 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, self.pseudoclient.uid, chan)
# PyLink internal hook called when spawnMain is called and the
# contents of Irc().pseudoclient change.
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
if __name__ == '__main__': if __name__ == '__main__':
log.info('PyLink starting...') log.info('PyLink starting...')

View File

@ -62,6 +62,7 @@ def quit(irc, source, args):
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client quit' quitmsg = ' '.join(args[1:]) or 'Client quit'
irc.proto.quitClient(irc, u, quitmsg) irc.proto.quitClient(irc, u, quitmsg)
irc.callHooks([u, 'PYLINK_ADMIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
def joinclient(irc, source, args): def joinclient(irc, source, args):
"""<target> <channel1>,[<channel2>], etc. """<target> <channel1>,[<channel2>], etc.
@ -82,6 +83,9 @@ def joinclient(irc, source, args):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) utils.msg(irc, source, "Error: Invalid channel name %r." % channel)
return return
irc.proto.joinClient(irc, u, channel) irc.proto.joinClient(irc, u, channel)
irc.callHooks([u, 'PYLINK_ADMIN_JOIN', {'channel': channel, 'users': [u],
'modes': irc.channels[channel].modes,
'parse_as': 'JOIN'}])
utils.add_cmd(joinclient, name='join') utils.add_cmd(joinclient, name='join')
@utils.add_cmd @utils.add_cmd
@ -103,6 +107,7 @@ def nick(irc, source, args):
utils.msg(irc, source, 'Error: Invalid nickname %r.' % newnick) utils.msg(irc, source, 'Error: Invalid nickname %r.' % newnick)
return return
irc.proto.nickClient(irc, u, newnick) irc.proto.nickClient(irc, u, newnick)
irc.callHooks([u, 'PYLINK_ADMIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
@utils.add_cmd @utils.add_cmd
def part(irc, source, args): def part(irc, source, args):
@ -123,6 +128,7 @@ def part(irc, source, args):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) utils.msg(irc, source, "Error: Invalid channel name %r." % channel)
return return
irc.proto.partClient(irc, u, channel, reason) irc.proto.partClient(irc, u, channel, reason)
irc.callHooks([u, 'PYLINK_ADMIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
@utils.add_cmd @utils.add_cmd
def kick(irc, source, args): def kick(irc, source, args):
@ -144,6 +150,7 @@ def kick(irc, source, args):
utils.msg(irc, source, "Error: Invalid channel name %r." % channel) utils.msg(irc, source, "Error: Invalid channel name %r." % channel)
return return
irc.proto.kickClient(irc, u, channel, targetu, reason) irc.proto.kickClient(irc, u, channel, targetu, reason)
irc.callHooks([u, 'PYLINK_ADMIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}])
@utils.add_cmd @utils.add_cmd
def showuser(irc, source, args): def showuser(irc, source, args):
@ -186,7 +193,7 @@ def showchan(irc, source, args):
def mode(irc, source, args): def mode(irc, source, args):
"""<source> <target> <modes> """<source> <target> <modes>
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is the nick of a PyLink client.""" Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
modesource, target, modes = args[0], args[1], args[2:] modesource, target, modes = args[0], args[1], args[2:]
@ -205,6 +212,36 @@ def mode(irc, source, args):
return return
if utils.isInternalServer(irc, modesource): if utils.isInternalServer(irc, modesource):
irc.proto.modeServer(irc, modesource, target, parsedmodes) irc.proto.modeServer(irc, modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_ADMIN_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(irc, sourceuid, target, parsedmodes)
irc.callHooks([sourceuid, 'PYLINK_ADMIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
@utils.add_cmd
def msg(irc, source, args):
"""<source> <target> <text>
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source)
try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError:
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.')
return
sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid:
utils.msg(irc, source, 'Error: unknown user %r' % msgsource)
return
if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target)
if real_target is None:
utils.msg(irc, source, 'Error: unknown user %r' % target)
return
else:
real_target = target
if not text:
utils.msg(irc, source, 'Error: no text given.')
return
irc.proto.messageClient(irc, sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_ADMIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])

View File

@ -16,7 +16,9 @@ dbname = "pylinkrelay"
if confname != 'pylink': if confname != 'pylink':
dbname += '-%s' % confname dbname += '-%s' % confname
dbname += '.db' dbname += '.db'
relayusers = defaultdict(dict) relayusers = defaultdict(dict)
spawnlocks = defaultdict(threading.Lock)
def relayWhoisHandlers(irc, target): def relayWhoisHandlers(irc, target):
user = irc.users[target] user = irc.users[target]
@ -32,7 +34,6 @@ utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, oldnick=''): def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
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.proto.__name__
maxnicklen = irc.maxnicklen maxnicklen = irc.maxnicklen
@ -59,14 +60,14 @@ def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
nick += suffix nick += suffix
# FIXME: factorize # FIXME: factorize
while utils.nickToUid(irc, nick) or utils.nickToUid(irc, oldnick) and not \ while utils.nickToUid(irc, nick) or utils.nickToUid(irc, oldnick) and not \
utils.isInternalClient(irc, utils.nickToUid(irc, nick)): isRelayClient(irc, utils.nickToUid(irc, nick)):
# The nick we want exists? Darn, create another one then, but only if # The nick we want exists? Darn, create another one then, but only if
# the target isn't an internal client! # the target isn't an internal client!
# Increase the separator length by 1 if the user was already tagged, # Increase the separator length by 1 if the user was already tagged,
# but couldn't be created due to a nick conflict. # but couldn't be created due to a nick conflict.
# This can happen when someone steals a relay user's nick. # This can happen when someone steals a relay user's nick.
new_sep = separator + separator[-1] new_sep = separator + separator[-1]
log.debug('(%s) normalizeNick: using %r as new_sep.', irc.name, separator) log.debug('(%s) normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep)
nick = normalizeNick(irc, netname, orig_nick, separator=new_sep) nick = normalizeNick(irc, netname, orig_nick, separator=new_sep)
finalLength = len(nick) finalLength = len(nick)
assert finalLength <= maxnicklen, "Normalized nick %r went over max " \ assert finalLength <= maxnicklen, "Normalized nick %r went over max " \
@ -109,7 +110,8 @@ def getPrefixModes(irc, remoteirc, channel, user):
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'): for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
if pmode in remoteirc.cmodes: # Mode supported by IRCd if pmode in remoteirc.cmodes: # Mode supported by IRCd
mlist = irc.channels[channel].prefixmodes[pmode+'s'] mlist = irc.channels[channel].prefixmodes[pmode+'s']
log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist) log.debug('(%s) getPrefixModes: checking if %r is in %s list: %r',
irc.name, user, pmode, mlist)
if user in mlist: if user in mlist:
modes += remoteirc.cmodes[pmode] modes += remoteirc.cmodes[pmode]
return modes return modes
@ -123,6 +125,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
return remoteirc.pseudoclient.uid return remoteirc.pseudoclient.uid
except AttributeError: # Network hasn't been initialized yet? except AttributeError: # Network hasn't been initialized yet?
pass pass
with spawnlocks[irc.name]:
try: try:
u = relayusers[(irc.name, user)][remoteirc.name] u = relayusers[(irc.name, user)][remoteirc.name]
except KeyError: except KeyError:
@ -141,7 +144,10 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
host=host, realname=realname, host=host, realname=realname,
modes=modes, ts=userobj.ts).uid modes=modes, ts=userobj.ts).uid
remoteirc.users[u].remote = irc.name remoteirc.users[u].remote = (irc.name, user)
away = userobj.away
if away:
remoteirc.proto.awayClient(remoteirc, u, away)
relayusers[(irc.name, user)][remoteirc.name] = u relayusers[(irc.name, user)][remoteirc.name] = u
return u return u
@ -156,23 +162,10 @@ def getLocalUser(irc, user, targetirc=None):
representing the original user on the target network, similar to what representing the original user on the target network, similar to what
getRemoteUser() does.""" getRemoteUser() does."""
# First, iterate over everyone! # First, iterate over everyone!
try:
remoteuser = irc.users[user].remote
except (AttributeError, KeyError):
remoteuser = None remoteuser = None
for k, v in relayusers.items():
log.debug('(%s) getLocalUser: processing %s, %s in relayusers', irc.name, k, v)
if k[0] == irc.name:
# We don't need to do anything if the target users is on
# the same network as us.
log.debug('(%s) getLocalUser: skipping %s since the target network matches the source network.', irc.name, k)
continue
if v.get(irc.name) == user:
# If the stored pseudoclient UID for the kicked user on
# this network matches the target we have, set that user
# as the one we're kicking! It's a handful, but remember
# we're mapping (home network, UID) pairs to their
# respective relay pseudoclients on other networks.
remoteuser = k
log.debug('(%s) getLocalUser: found %s to correspond to %s.', irc.name, v, k)
break
log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name) log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name)
if remoteuser: if remoteuser:
# If targetirc is given, we'll return simply the UID of the user on the # If targetirc is given, we'll return simply the UID of the user on the
@ -221,6 +214,11 @@ def initializeChannel(irc, channel):
log.debug('(%s) initializeChannel: relay pair found to be %s', irc.name, relay) log.debug('(%s) initializeChannel: relay pair found to be %s', irc.name, relay)
queued_users = [] queued_users = []
if relay: if relay:
# Send our users and channel modes to the other nets
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
relayJoins(irc, channel, c.users, c.ts)
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel)
all_links = db[relay]['links'].copy() all_links = db[relay]['links'].copy()
all_links.update((relay,)) all_links.update((relay,))
log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links) log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links)
@ -237,37 +235,30 @@ def initializeChannel(irc, channel):
if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)): if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)):
continue # They aren't connected, don't bother! continue # They aren't connected, don't bother!
# Join their (remote) users and set their modes. # Join their (remote) users and set their modes.
relayJoins(remoteirc, remotechan, rc.users, relayJoins(remoteirc, remotechan, rc.users, rc.ts)
rc.ts, rc.modes) relayModes(remoteirc, irc, remoteirc.sid, remotechan, rc.modes)
relayModes(irc, remoteirc, irc.sid, channel) relayModes(irc, remoteirc, irc.sid, channel, modes)
topic = remoteirc.channels[relay[1]].topic topic = remoteirc.channels[remotechan].topic
# 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[channel].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, irc.sid, channel, topic)
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
# After that's done, we'll send our users to them.
relayJoins(irc, channel, c.users, c.ts, c.modes)
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel)
def handle_join(irc, numeric, command, args): def handle_join(irc, numeric, command, args):
channel = args['channel'] channel = args['channel']
if not findRelay((irc.name, channel)): if not findRelay((irc.name, channel)):
# No relay here, return. # No relay here, return.
return return
modes = args['modes']
ts = args['ts'] ts = args['ts']
users = set(args['users']) users = set(args['users'])
relayJoins(irc, channel, users, ts, modes) relayJoins(irc, channel, users, ts)
utils.add_hook(handle_join, 'JOIN') utils.add_hook(handle_join, 'JOIN')
def handle_quit(irc, numeric, command, args): def handle_quit(irc, numeric, command, args):
ouruser = numeric
for netname, user in relayusers[(irc.name, numeric)].copy().items(): for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
remoteirc.proto.quitClient(remoteirc, user, args['text']) remoteirc.proto.quitClient(remoteirc, user, args['text'])
del relayusers[(irc.name, ouruser)] del relayusers[(irc.name, numeric)]
utils.add_hook(handle_quit, 'QUIT') utils.add_hook(handle_quit, 'QUIT')
def handle_squit(irc, numeric, command, args): def handle_squit(irc, numeric, command, args):
@ -288,6 +279,9 @@ utils.add_hook(handle_nick, 'NICK')
def handle_part(irc, numeric, command, args): def handle_part(irc, numeric, command, args):
channels = args['channels'] channels = args['channels']
text = args['text'] text = args['text']
# Don't allow the PyLink client PARTing to be relayed.
if numeric == irc.pseudoclient.uid:
return
for channel in channels: for channel in channels:
for netname, user in relayusers[(irc.name, numeric)].copy().items(): for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
@ -306,20 +300,8 @@ def handle_privmsg(irc, numeric, command, args):
text = args['text'] text = args['text']
if target == irc.pseudoclient.uid: if target == irc.pseudoclient.uid:
return return
sent = 0
relay = findRelay((irc.name, target)) relay = findRelay((irc.name, target))
# Don't send any "you must be in common channels" if we're not part remoteusers = relayusers[(irc.name, numeric)]
# of a relay, or we are but there are no links!
remoteusers = relayusers[(irc.name, numeric)].items()
'''
if utils.isChannel(target) and ((relay and not db[relay]['links']) or \
relay is None):
return
'''
if not remoteusers:
return
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
# HACK: Don't break on sending to @#channel or similar. # HACK: Don't break on sending to @#channel or similar.
try: try:
prefix, target = target.split('#', 1) prefix, target = target.split('#', 1)
@ -327,36 +309,46 @@ def handle_privmsg(irc, numeric, command, args):
prefix = '' prefix = ''
else: else:
target = '#' + target target = '#' + target
if utils.isChannel(target):
log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target)
if utils.isChannel(target) and relay and numeric not in irc.channels[target].users:
# The sender must be in the target channel to send messages over the relay;
# 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
# 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 '
'messages over the relay.' % target, notice=True)
return
if utils.isChannel(target):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
real_target = findRemoteChan(irc, remoteirc, target) real_target = findRemoteChan(irc, remoteirc, target)
if not real_target: if not real_target:
continue continue
real_target = prefix + real_target real_target = prefix + real_target
else:
remoteuser = getLocalUser(irc, target)
if remoteuser is None:
continue
real_target = remoteuser[1]
if notice: if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text) remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
else: else:
remoteirc.proto.messageClient(remoteirc, user, real_target, text) remoteirc.proto.messageClient(remoteirc, user, real_target, text)
sent += 1
'''
if not sent:
# We must be on a common channel with the target. Otherwise, the sender
# doesn't have a client representing them on the remote network,
# and we won't have anywhere to send our messages from.
# In this case, we've iterated over all networks where the sender
# has pseudoclients, and found no suitable targets to send to.
if target in irc.users:
target_s = 'a common channel with %r' % irc.users[target].nick
else: else:
target_s = repr(target) remoteuser = getLocalUser(irc, target)
utils.msg(irc, numeric, 'Error: You must be in %s in order to send messages.' % \ if remoteuser is None:
target_s, notice=True) return
''' homenet, real_target = remoteuser
# For PMs, we must be on a common channel with the target.
# Otherwise, the sender doesn't have a client representing them
# on the remote network, and we won't have anything to send our
# messages from.
if homenet not in remoteusers.keys():
utils.msg(irc, numeric, 'Error: you must be in a common channel '
'with %r in order to send messages.' % \
irc.users[target].nick, notice=True)
return
remoteirc = utils.networkobjects[homenet]
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
else:
remoteirc.proto.messageClient(remoteirc, 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')
@ -367,8 +359,10 @@ def handle_kick(irc, source, command, args):
kicker = source kicker = source
kicker_modes = getPrefixModes(irc, irc, channel, kicker) kicker_modes = getPrefixModes(irc, irc, channel, kicker)
relay = findRelay((irc.name, channel)) relay = findRelay((irc.name, channel))
if relay is None: # Don't allow kicks to the PyLink client to be relayed.
if relay is None or target == irc.pseudoclient.uid:
return return
origuser = getLocalUser(irc, target)
for name, remoteirc in utils.networkobjects.items(): for name, remoteirc in utils.networkobjects.items():
if irc.name == name or not remoteirc.connected.is_set(): if irc.name == name or not remoteirc.connected.is_set():
continue continue
@ -378,20 +372,21 @@ def handle_kick(irc, source, command, args):
continue continue
real_kicker = getRemoteUser(irc, remoteirc, kicker, spawnIfMissing=False) real_kicker = getRemoteUser(irc, remoteirc, kicker, spawnIfMissing=False)
log.debug('(%s) Relay kick: real kicker for %s on %s is %s', irc.name, kicker, name, real_kicker) log.debug('(%s) Relay kick: real kicker for %s on %s is %s', irc.name, kicker, name, real_kicker)
if not utils.isInternalClient(irc, target): if not isRelayClient(irc, target):
log.debug('(%s) Relay kick: target %s is NOT an internal client', irc.name, target) log.debug('(%s) Relay kick: target %s is NOT an internal client', irc.name, target)
# Both the target and kicker are external clients; i.e. # Both the target and kicker are external clients; i.e.
# they originate from the same network. We won't have # they originate from the same network. We won't have
# to filter this; the uplink IRCd will handle it appropriately, # to filter this; the uplink IRCd will handle it appropriately,
# and we'll just follow. # and we'll just follow.
real_target = getRemoteUser(irc, remoteirc, target) real_target = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target) log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target)
else: else:
log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target) log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target)
real_target = getLocalUser(irc, target, targetirc=remoteirc) real_target = getLocalUser(irc, target, targetirc=remoteirc)
log.debug('(%s) Relay kick: kicker_modes are %r', irc.name, kicker_modes) log.debug('(%s) Relay kick: kicker_modes are %r', irc.name, kicker_modes)
if irc.name not in db[relay]['claim'] and not \ if irc.name not in db[relay]['claim'] and not \
any([mode in kicker_modes for mode in ('y', 'q', 'a', 'o', 'h')]): (any([mode in kicker_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \
or utils.isInternalClient(irc, kicker)):
log.debug('(%s) Relay kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target) log.debug('(%s) Relay kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
# Home network is not in the channel's claim AND the kicker is not # Home network is not in the channel's claim AND the kicker is not
# opped. We won't propograte the kick then. # opped. We won't propograte the kick then.
@ -399,13 +394,15 @@ 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, remotechan, [(modes, target)]) irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)])
if kicker in irc.users: if kicker in irc.users:
utils.msg(irc, kicker, "This channel is claimed; your kick has " utils.msg(irc, kicker, "This channel is claimed; your kick to "
"to %s been blocked because you are not " "%s has been blocked because you are not "
"(half)opped." % channel, notice=True) "(half)opped." % channel, notice=True)
return return
if not real_target:
continue
# 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)
@ -426,10 +423,14 @@ def handle_kick(irc, source, command, args):
remoteirc.proto.kickServer(remoteirc, remoteirc.sid, remoteirc.proto.kickServer(remoteirc, remoteirc.sid,
remotechan, real_target, text) remotechan, real_target, text)
if target != irc.pseudoclient.uid and not irc.users[target].channels: # If the target isn't on any channels, quit them.
if origuser and origuser[0] != remoteirc.name and not remoteirc.users[real_target].channels:
del relayusers[origuser][remoteirc.name]
remoteirc.proto.quitClient(remoteirc, real_target, 'Left all shared channels.')
if origuser and not irc.users[target].channels:
del relayusers[origuser][irc.name]
irc.proto.quitClient(irc, target, 'Left all shared channels.') irc.proto.quitClient(irc, target, 'Left all shared channels.')
remoteuser = getLocalUser(irc, target)
del relayusers[remoteuser][irc.name]
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
@ -610,7 +611,7 @@ utils.add_hook(handle_topic, 'TOPIC')
def handle_kill(irc, numeric, command, args): def handle_kill(irc, numeric, command, args):
target = args['target'] target = args['target']
userdata = args['userdata'] userdata = args['userdata']
realuser = getLocalUser(irc, target) realuser = getLocalUser(irc, target) or userdata.__dict__.get('remote')
log.debug('(%s) relay handle_kill: realuser is %r', irc.name, realuser) log.debug('(%s) relay handle_kill: realuser is %r', irc.name, realuser)
# Target user was remote: # Target user was remote:
if realuser and realuser[0] != irc.name: if realuser and realuser[0] != irc.name:
@ -618,15 +619,15 @@ def handle_kill(irc, numeric, command, args):
# client and rejoin it to its channels. # client and rejoin it to its channels.
del relayusers[realuser][irc.name] del relayusers[realuser][irc.name]
remoteirc = utils.networkobjects[realuser[0]] remoteirc = utils.networkobjects[realuser[0]]
for channel in remoteirc.channels: for remotechan in remoteirc.channels:
remotechan = findRemoteChan(remoteirc, irc, channel) localchan = findRemoteChan(remoteirc, irc, remotechan)
if remotechan: if localchan:
modes = getPrefixModes(remoteirc, irc, remotechan, 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, remotechan, [(modes, client)]) irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)])
if userdata and numeric in irc.users: if userdata and numeric in irc.users:
utils.msg(irc, numeric, "Your kill has to %s been blocked " utils.msg(irc, 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)
@ -641,7 +642,17 @@ def handle_kill(irc, numeric, command, args):
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
def relayJoins(irc, channel, users, ts, modes): def isRelayClient(irc, user):
try:
if irc.users[user].remote:
# Is the .remote attribute set? If so, don't relay already
# relayed clients; that'll trigger an endless loop!
return True
except (KeyError, AttributeError): # Nope, it isn't.
pass
return False
def relayJoins(irc, channel, users, ts):
for name, remoteirc in utils.networkobjects.items(): for name, remoteirc in utils.networkobjects.items():
queued_users = [] queued_users = []
if name == irc.name or not remoteirc.connected.is_set(): if name == irc.name or not remoteirc.connected.is_set():
@ -654,23 +665,16 @@ def relayJoins(irc, channel, users, ts, modes):
continue continue
log.debug('(%s) relayJoins: got %r for users', irc.name, users) log.debug('(%s) relayJoins: got %r for users', irc.name, users)
for user in users.copy(): for user in users.copy():
if utils.isInternalClient(irc, user) or user not in irc.users: if isRelayClient(irc, user):
# We don't need to clone PyLink pseudoclients... That's # Don't clone relay clients; that'll cause some bad, bad
# meaningless. # things to happen.
continue continue
try:
if irc.users[user].remote:
# Is the .remote attribute set? If so, don't relay already
# relayed clients; that'll trigger an endless loop!
continue
except AttributeError: # Nope, it isn't.
pass
log.debug('Okay, spawning %s/%s everywhere', user, irc.name) log.debug('Okay, spawning %s/%s everywhere', user, irc.name)
assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user) assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user)
u = getRemoteUser(irc, remoteirc, user) u = getRemoteUser(irc, remoteirc, user)
# Only join users if they aren't already joined. This prevents op floods # Only join users if they aren't already joined. This prevents op floods
# on charybdis from all the SJOINing. # on charybdis from all the SJOINing.
if u not in remoteirc.channels[channel].users: if u not in remoteirc.channels[remotechan].users:
ts = irc.channels[channel].ts ts = irc.channels[channel].ts
prefixes = getPrefixModes(irc, remoteirc, channel, user) prefixes = getPrefixModes(irc, remoteirc, channel, user)
userpair = (prefixes, u) userpair = (prefixes, u)
@ -681,7 +685,6 @@ def relayJoins(irc, channel, users, ts, modes):
u, remoteirc.name, remotechan) u, remoteirc.name, remotechan)
if queued_users: if queued_users:
remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, queued_users, ts=ts) remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, queued_users, ts=ts)
relayModes(irc, remoteirc, irc.sid, channel, modes)
def relayPart(irc, channel, user): def relayPart(irc, channel, user):
for name, remoteirc in utils.networkobjects.items(): for name, remoteirc in utils.networkobjects.items():
@ -696,7 +699,7 @@ def relayPart(irc, channel, user):
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(remoteirc, remoteuser, remotechan, 'Channel delinked.')
if 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(remoteirc, remoteuser, 'Left all shared channels.')
del relayusers[(irc.name, user)][remoteirc.name] del relayusers[(irc.name, user)][remoteirc.name]
@ -708,7 +711,7 @@ def removeChannel(irc, channel):
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():
if not utils.isInternalClient(irc, user): if not isRelayClient(irc, user):
relayPart(irc, channel, user) relayPart(irc, channel, user)
# Don't ever part the main client from any of its autojoin channels. # Don't ever part the main client from any of its autojoin channels.
else: else:
@ -718,9 +721,9 @@ def removeChannel(irc, channel):
irc.proto.partClient(irc, user, channel, 'Channel delinked.') irc.proto.partClient(irc, 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:
irc.proto.quitClient(irc, user, 'Left all shared 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.')
@utils.add_cmd @utils.add_cmd
def create(irc, source, args): def create(irc, source, args):
@ -847,7 +850,10 @@ def delink(irc, source, args):
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 delink, or use the 'destroy' command no remove this relay entirely.") utils.msg(irc, source, "Error: You must select a network to "
"delink, or use the 'destroy' command to remove "
"this relay entirely (it was created on the current "
"network).")
return return
else: else:
for link in db[entry]['links'].copy(): for link in db[entry]['links'].copy():
@ -900,7 +906,7 @@ def handle_save(irc, numeric, command, args):
realuser = getLocalUser(irc, target) realuser = getLocalUser(irc, target)
log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r', log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r',
irc.name, target, realuser) irc.name, target, realuser)
if utils.isInternalClient(irc, target) and realuser: if isRelayClient(irc, target) and realuser:
# Nick collision! # Nick collision!
# It's one of our relay clients; try to fix our nick to the next # It's one of our relay clients; try to fix our nick to the next
# available normalized nick. # available normalized nick.
@ -933,3 +939,16 @@ def linked(irc, source, args):
else: else:
s += '(no relays yet)' s += '(no relays yet)'
utils.msg(irc, source, s) utils.msg(irc, source, s)
def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text'])
utils.add_hook(handle_away, 'AWAY')
def handle_spawnmain(irc, numeric, command, args):
if args['olduser']:
# Kills to the main PyLink client force reinitialization; this makes sure
# it joins all the relay channels like it's supposed to.
initializeAll(irc)
utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN')

View File

@ -56,10 +56,11 @@ def joinClient(irc, client, channel):
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)', irc.name, client, channel)
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
# One channel per line here! # 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']]
_send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format( _send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
ts=irc.channels[channel].ts, uid=client, channel=channel, ts=irc.channels[channel].ts, uid=client, channel=channel,
modes=utils.joinModes(irc.channels[channel].modes))) modes=utils.joinModes(modes)))
irc.channels[channel].users.add(client) irc.channels[channel].users.add(client)
irc.users[client].channels.add(channel) irc.users[client].channels.add(channel)
@ -70,17 +71,21 @@ def sjoinServer(irc, server, channel, users, ts=None):
log.debug('(%s) sjoinServer: got %r for users', irc.name, users) log.debug('(%s) sjoinServer: got %r for users', irc.name, users)
if not server: if not server:
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
if ts is None: orig_ts = irc.channels[channel].ts
ts = irc.channels[channel].ts ts = ts or orig_ts
if ts < orig_ts:
# 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.
log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)',
irc.name, channel, orig_ts, ts)
irc.channels[channel].ts = ts
irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values():
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, irc.name, ts,
time.strftime("%c", time.localtime(ts))) time.strftime("%c", time.localtime(ts)))
''' TODO: handle this properly! # Strip out list-modes, they shouldn't be ever sent in FJOIN.
if modes is None: modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']]
modes = irc.channels[channel].modes
else:
utils.applyModes(irc, channel, modes)
'''
modes = irc.channels[channel].modes
uids = [] uids = []
changedmodes = [] changedmodes = []
namelist = [] namelist = []
@ -96,6 +101,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
irc.users[user].channels.add(channel) 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?", irc.name, channel, user)
if ts < orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(irc, channel, changedmodes)
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
_send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format( _send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format(
@ -119,8 +126,12 @@ def removeClient(irc, numeric):
Removes a client from our internal databases, regardless Removes a client from our internal databases, regardless
of whether it's one of our pseudoclients or not.""" of whether it's one of our pseudoclients or not."""
for v in irc.channels.values(): for c, v in irc.channels.copy().items():
v.removeuser(numeric) v.removeuser(numeric)
# Clear empty non-permanent channels.
if not (irc.channels[c].users or ((irc.cmodes.get('permanent'), None) in irc.channels[c].modes)):
del irc.channels[c]
sid = numeric[:3] sid = numeric[:3]
log.debug('Removing client %s from irc.users', numeric) log.debug('Removing client %s from irc.users', numeric)
del irc.users[numeric] del irc.users[numeric]
@ -244,6 +255,8 @@ def topicClient(irc, numeric, target, text):
if not utils.isInternalClient(irc, numeric): if not utils.isInternalClient(irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
_send(irc, numeric, 'TOPIC %s :%s' % (target, text)) _send(irc, numeric, 'TOPIC %s :%s' % (target, text))
irc.channels[target].topic = text
irc.channels[target].topicset = True
def topicServer(irc, numeric, target, text): def topicServer(irc, numeric, target, text):
if not utils.isInternalServer(irc, numeric): if not utils.isInternalServer(irc, numeric):
@ -251,6 +264,8 @@ def topicServer(irc, numeric, target, text):
ts = int(time.time()) ts = int(time.time())
servername = irc.servers[numeric].name servername = irc.servers[numeric].name
_send(irc, numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) _send(irc, numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text))
irc.channels[target].topic = text
irc.channels[target].topicset = True
def inviteClient(irc, numeric, target, channel): def inviteClient(irc, numeric, target, channel):
"""<irc object> <client numeric> <text> """<irc object> <client numeric> <text>
@ -297,6 +312,16 @@ def numericServer(irc, source, numeric, text):
"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):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY %s :%s' % (int(time.time()), text))
else:
_send(irc, source, 'AWAY')
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
@ -349,6 +374,9 @@ def handle_part(irc, source, command, args):
reason = args[1] reason = args[1]
except IndexError: except IndexError:
reason = '' reason = ''
# Clear empty non-permanent channels.
if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)):
del irc.channels[channel]
return {'channels': channels, 'text': reason} return {'channels': channels, 'text': reason}
def handle_error(irc, numeric, command, args): def handle_error(irc, numeric, command, args):
@ -367,6 +395,9 @@ def handle_fjoin(irc, servernumeric, command, args):
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) irc.name, channel, their_ts, our_ts)
irc.channels[channel].ts = their_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] modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(irc, channel, modestring) parsedmodes = utils.parseModes(irc, channel, modestring)
utils.applyModes(irc, channel, parsedmodes) utils.applyModes(irc, channel, parsedmodes)
@ -664,3 +695,13 @@ def handle_fname(irc, numeric, command, args):
def handle_endburst(irc, numeric, command, args): def handle_endburst(irc, numeric, command, args):
return {} return {}
def handle_away(irc, numeric, command, args):
# <- :1MLAAAAIG AWAY 1439371390 :Auto-away
try:
ts = args[0]
irc.users[numeric].away = text = args[1]
return {'text': text, 'ts': ts}
except IndexError: # User is unsetting away status
irc.users[numeric].away = ''
return {'text': ''}

View File

@ -77,8 +77,17 @@ def sjoinServer(irc, server, channel, users, ts=None):
log.debug('(%s) sjoinServer: got %r for users', irc.name, users) log.debug('(%s) sjoinServer: got %r for users', irc.name, users)
if not server: if not server:
raise LookupError('No such PyLink PseudoClient exists.') raise LookupError('No such PyLink PseudoClient exists.')
if ts is None: orig_ts = irc.channels[channel].ts
ts = irc.channels[channel].ts ts = ts or orig_ts
if ts < orig_ts:
# 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.
log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)',
irc.name, channel, orig_ts, ts)
irc.channels[channel].ts = ts
irc.channels[channel].modes.clear()
for p in irc.channels[channel].prefixmodes.values():
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, 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 irc.channels[channel].modes if m[0] not in irc.cmodes['*A']]
@ -95,6 +104,7 @@ def sjoinServer(irc, server, channel, users, ts=None):
pr = irc.prefixmodes.get(prefix) pr = irc.prefixmodes.get(prefix)
if pr: if pr:
prefixchars += pr prefixchars += pr
changedmodes.append(('+%s' % prefix, user))
namelist.append(prefixchars+user) namelist.append(prefixchars+user)
uids.append(user) uids.append(user)
try: try:
@ -107,6 +117,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
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) irc.channels[channel].users.update(uids)
if ts < orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than theirs.
utils.applyModes(irc, channel, changedmodes) utils.applyModes(irc, channel, changedmodes)
def _sendModes(irc, numeric, target, modes, ts=None): def _sendModes(irc, numeric, target, modes, ts=None):
@ -186,6 +198,8 @@ def topicServer(irc, numeric, target, text):
ts = irc.channels[target].ts ts = irc.channels[target].ts
servername = irc.servers[numeric].name servername = irc.servers[numeric].name
_send(irc, numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) _send(irc, numeric, 'TB %s %s %s :%s' % (target, ts, servername, text))
irc.channels[target].topic = text
irc.channels[target].topicset = True
def inviteClient(irc, numeric, target, channel): def inviteClient(irc, numeric, target, channel):
"""<irc object> <client numeric> <text> """<irc object> <client numeric> <text>
@ -231,6 +245,16 @@ def pingServer(irc, source=None, target=None):
def numericServer(irc, source, numeric, target, text): def numericServer(irc, source, numeric, target, text):
_send(irc, source, '%s %s %s' % (numeric, target, text)) _send(irc, source, '%s %s %s' % (numeric, target, text))
def awayClient(irc, source, text):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY :%s' % text)
else:
_send(irc, source, 'AWAY')
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
@ -358,9 +382,10 @@ def handle_part(irc, source, command, args):
reason = args[1] reason = args[1]
except IndexError: except IndexError:
reason = '' reason = ''
if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)):
del irc.channels[channel]
return {'channels': channels, 'text': reason} return {'channels': channels, 'text': reason}
def handle_sjoin(irc, servernumeric, command, args): def handle_sjoin(irc, servernumeric, command, args):
# parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist
channel = args[1].lower() channel = args[1].lower()
@ -372,6 +397,9 @@ def handle_sjoin(irc, servernumeric, command, args):
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) irc.name, channel, their_ts, our_ts)
irc.channels[channel].ts = their_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] modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(irc, channel, modestring) parsedmodes = utils.parseModes(irc, channel, modestring)
utils.applyModes(irc, channel, parsedmodes) utils.applyModes(irc, channel, parsedmodes)
@ -649,3 +677,13 @@ def handle_472(irc, numeric, command, args):
' 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.', irc.name, setter, badmode,
charlist[badmode]) charlist[badmode])
def handle_away(irc, numeric, command, args):
# <- :6ELAAAAAB AWAY :Auto-away
try:
irc.users[numeric].away = text = args[0]
except IndexError: # User is unsetting away status
irc.users[numeric].away = text = ''
return {'text': text}

View File

@ -1,18 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Shell script to start PyLink under CPUlimit, killing it if it starts abusing the CPU. # Shell script to start PyLink under CPUlimit, throttling it if it starts abusing the CPU.
# Set this to whatever you want. cpulimit --help # Set this to whatever you want. cpulimit --help
LIMIT=20 LIMIT=35
# Change to the PyLink root directory. # Change to the PyLink root directory.
WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$WRAPPER_DIR" cd "$WRAPPER_DIR"
if [[ ! -z "$(which cpulimit)" ]]; then if [[ ! -z "$(which cpulimit)" ]]; then
# -k kills the PyLink daemon if it goes over $LIMIT
# -z makes cpulimit exit when PyLink dies. # -z makes cpulimit exit when PyLink dies.
cpulimit -l $LIMIT -z -k ./main.py cpulimit -l $LIMIT -z ./main.py
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be killed 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
echo 'cpulimit not found in $PATH! Aborting.' echo 'cpulimit not found in $PATH! Aborting.'

View File

@ -241,7 +241,10 @@ def applyModes(irc, target, changedmodes):
log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', irc.name, changedmodes, target, modelist) log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', irc.name, changedmodes, target, modelist)
for mode in changedmodes: for mode in changedmodes:
# Chop off the +/- part that parseModes gives; it's meaningless for a mode list. # Chop off the +/- part that parseModes gives; it's meaningless for a mode list.
try:
real_mode = (mode[0][1], mode[1]) real_mode = (mode[0][1], mode[1])
except IndexError:
real_mode = mode
if not usermodes: if not usermodes:
pmode = '' pmode = ''
for m in ('owner', 'admin', 'op', 'halfop', 'voice'): for m in ('owner', 'admin', 'op', 'halfop', 'voice'):