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

Merge branch 'devel' into wip/handle-ts-bursts

Conflicts:
	plugins/relay.py
This commit is contained in:
James Lu 2015-08-15 19:28:49 -07:00
commit 0d41e35d9c
10 changed files with 263 additions and 139 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

78
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
@ -18,6 +20,8 @@ class Irc():
def initVars(self): def initVars(self):
# 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.connected = threading.Event()
self.lastping = time.time()
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 = {}
self.channels = defaultdict(classes.IrcChannel) self.channels = defaultdict(classes.IrcChannel)
@ -52,7 +56,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 +70,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))
@ -112,14 +167,15 @@ class Irc():
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')

View File

@ -186,7 +186,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:]
@ -208,3 +208,30 @@ def mode(irc, source, args):
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)
@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)

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]
@ -30,13 +32,6 @@ def relayWhoisHandlers(irc, target):
utils.whois_handlers.append(relayWhoisHandlers) utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, oldnick=''): def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
# Block until we know the IRC network's nick length (after capabilities
# are sent)
if not hasattr(irc, 'relay_waitFinished'):
log.debug('(%s) normalizeNick: waiting for irc.connected', irc.name)
irc.connected.wait(1)
irc.relay_waitFinished = True
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)
@ -49,7 +44,7 @@ def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
separator = separator.replace('/', '|') separator = separator.replace('/', '|')
nick = nick.replace('/', '|') nick = nick.replace('/', '|')
if nick.startswith(tuple(string.digits)): if nick.startswith(tuple(string.digits)):
# On TS6 IRCd-s, nicks that start with 0-9 are only allowed if # On TS6 IRCds, nicks that start with 0-9 are only allowed if
# they match the UID of the originating server. Otherwise, you'll # they match the UID of the originating server. Otherwise, you'll
# get nasty protocol violations! # get nasty protocol violations!
nick = '_' + nick nick = '_' + nick
@ -116,7 +111,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
@ -130,11 +126,12 @@ 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:
userobj = irc.users.get(user) userobj = irc.users.get(user)
if userobj is None or (not spawnIfMissing) or (not remoteirc.connected): if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()):
# The query wasn't actually a valid user, or the network hasn't # The query wasn't actually a valid user, or the network hasn't
# been connected yet... Oh well! # been connected yet... Oh well!
return return
@ -148,7 +145,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
@ -163,23 +163,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
@ -241,7 +228,7 @@ def initializeChannel(irc, channel):
if remoteirc is None: if remoteirc is None:
continue continue
rc = remoteirc.channels[remotechan] rc = remoteirc.channels[remotechan]
if not (remoteirc.connected 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,
@ -313,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)
@ -334,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')
@ -391,7 +376,7 @@ def handle_kick(irc, source, command, args):
# 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)
@ -408,8 +393,8 @@ def handle_kick(irc, source, command, args):
# Join the kicked client back with its respective modes. # Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, target)]) irc.proto.sjoinServer(irc, irc.sid, remotechan, [(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
@ -434,9 +419,9 @@ def handle_kick(irc, source, command, args):
remotechan, real_target, text) remotechan, real_target, text)
if target != irc.pseudoclient.uid and not irc.users[target].channels: if target != irc.pseudoclient.uid and not irc.users[target].channels:
irc.proto.quitClient(irc, target, 'Left all shared channels.')
remoteuser = getLocalUser(irc, target) remoteuser = getLocalUser(irc, target)
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]
irc.proto.quitClient(irc, target, 'Left all shared channels.')
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
@ -639,7 +624,7 @@ def handle_kill(irc, numeric, command, args):
client = getRemoteUser(remoteirc, irc, realuser[1]) client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)]) irc.proto.sjoinServer(irc, irc.sid, remotechan, [(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)
@ -685,11 +670,9 @@ def relayJoins(irc, channel, users, ts, modes):
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:
if ts < rts: ts = irc.channels[channel].ts
prefixes = getPrefixModes(irc, remoteirc, channel, user) prefixes = getPrefixModes(irc, remoteirc, channel, user)
else:
prefixes = ''
userpair = (prefixes, u) userpair = (prefixes, u)
queued_users.append(userpair) queued_users.append(userpair)
log.debug('(%s) relayJoins: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan) log.debug('(%s) relayJoins: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan)
@ -735,9 +718,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):
@ -864,7 +847,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():
@ -950,3 +936,9 @@ 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')

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)
@ -74,13 +75,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
ts = irc.channels[channel].ts ts = irc.channels[channel].ts
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 = []
@ -297,6 +293,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
@ -667,3 +673,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

@ -230,6 +230,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
@ -651,3 +661,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.'