2015-07-11 05:26:46 +02:00
# relay.py: PyLink Relay plugin
import pickle
2015-12-29 20:13:50 +01:00
import time
2015-07-11 05:26:46 +02:00
import threading
2015-07-13 01:59:49 +02:00
import string
2015-07-13 08:28:54 +02:00
from collections import defaultdict
2015-07-11 05:26:46 +02:00
2016-07-08 07:16:21 +02:00
from pylinkirc import utils , world , conf
2016-06-21 03:18:54 +02:00
from pylinkirc . log import log
2016-07-14 03:49:51 +02:00
from pylinkirc . coremods import control
2015-07-11 05:26:46 +02:00
2015-12-07 02:13:47 +01:00
### GLOBAL (statekeeping) VARIABLES
2015-07-13 08:28:54 +02:00
relayusers = defaultdict ( dict )
2015-09-12 21:06:58 +02:00
relayservers = defaultdict ( dict )
spawnlocks = defaultdict ( threading . RLock )
2015-09-21 01:56:24 +02:00
spawnlocks_servers = defaultdict ( threading . RLock )
2016-03-26 00:39:06 +01:00
2016-02-28 03:13:26 +01:00
exportdb_timer = None
2016-07-08 07:16:21 +02:00
save_delay = conf . conf [ ' bot ' ] . get ( ' save_delay ' , 300 )
2016-06-17 06:55:50 +02:00
db = { }
2015-12-07 02:13:47 +01:00
dbname = utils . getDatabaseName ( ' pylinkrelay ' )
2015-09-15 02:23:56 +02:00
### INTERNAL FUNCTIONS
def initializeAll ( irc ) :
""" Initializes all relay channels for the given IRC object. """
2016-02-28 03:36:20 +01:00
# Wait for all IRC objects to be created first. This prevents
2015-10-11 00:21:38 +02:00
# relay servers from being spawned too early (before server authentication),
# which would break connections.
world . started . wait ( 2 )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
for chanpair , entrydata in db . items ( ) :
2016-02-28 03:36:20 +01:00
# Iterate over all the channels stored in our relay links DB.
2015-09-15 02:23:56 +02:00
network , channel = chanpair
2016-02-28 03:36:20 +01:00
# Initialize each relay channel on their home network, and on every linked one too.
2015-09-15 02:23:56 +02:00
initializeChannel ( irc , channel )
for link in entrydata [ ' links ' ] :
network , channel = link
initializeChannel ( irc , channel )
2015-09-27 20:56:09 +02:00
def main ( irc = None ) :
2015-09-15 02:23:56 +02:00
""" Main function, called during plugin loading at start. """
2015-12-27 02:06:28 +01:00
2016-02-28 03:13:26 +01:00
# Load the relay links database.
2015-09-15 02:23:56 +02:00
loadDB ( )
2015-12-27 02:06:28 +01:00
2016-05-01 23:09:17 +02:00
log . debug ( ' relay.main: loading links database ' )
2016-02-28 03:13:26 +01:00
# Schedule periodic exports of the links database.
scheduleExport ( starting = True )
2015-12-27 02:06:28 +01:00
2016-05-01 23:09:17 +02:00
log . debug ( ' relay.main: scheduling export loop ' )
2015-09-27 20:56:09 +02:00
if irc is not None :
2016-05-01 23:09:17 +02:00
# irc is defined when the plugin is reloaded. Otherwise, it means that we've just started the
# server. Iterate over all connected networks and initialize their relay users.
2015-09-27 20:56:09 +02:00
for ircobj in world . networkobjects . values ( ) :
2016-05-01 23:09:17 +02:00
if ircobj . connected . is_set ( ) :
initializeAll ( ircobj )
log . debug ( ' relay.main: finished initialization sequence ' )
2015-09-27 20:56:09 +02:00
def die ( sourceirc ) :
2015-12-27 02:06:28 +01:00
""" Deinitialize PyLink Relay by quitting all relay clients and saving the
relay DB . """
2016-02-28 03:13:26 +01:00
# For every connected network:
2015-09-27 20:56:09 +02:00
for irc in world . networkobjects . values ( ) :
2016-07-24 20:13:51 +02:00
# 1) SQUIT every relay subserver.
2015-09-27 20:56:09 +02:00
for server , sobj in irc . servers . copy ( ) . items ( ) :
if hasattr ( sobj , ' remote ' ) :
2016-01-17 01:53:06 +01:00
irc . proto . squit ( irc . sid , server , text = " Relay plugin unloaded. " )
2016-02-28 03:13:26 +01:00
2016-07-24 20:13:51 +02:00
# 2) Clear our internal servers and users caches.
2015-09-27 20:56:09 +02:00
relayservers . clear ( )
relayusers . clear ( )
2015-07-23 09:01:51 +02:00
2016-07-24 20:13:51 +02:00
# 3) Export the relay links database.
2016-02-28 03:13:26 +01:00
exportDB ( )
2016-07-24 20:13:51 +02:00
# 4) Kill the scheduling for any other exports.
2016-02-28 03:13:26 +01:00
global exportdb_timer
if exportdb_timer :
log . debug ( " Relay: cancelling exportDB timer thread %s due to die() " , threading . get_ident ( ) )
exportdb_timer . cancel ( )
2015-12-27 02:06:28 +01:00
2016-08-21 02:36:24 +02:00
allowed_chars = string . digits + string . ascii_letters + ' /^| \\ -_[] {} ` '
2016-07-02 04:16:47 +02:00
fallback_separator = ' | '
2016-07-11 08:32:04 +02:00
def normalizeNick ( irc , netname , nick , times_tagged = 0 , uid = ' ' ) :
"""
Creates a normalized nickname for the given nick suitable for introduction to a remote network
( as a relay client ) .
UID is optional for checking regular nick changes , to make sure that the sender doesn ' t get
marked as nick - colliding with itself .
"""
# Get the nick/net separator
separator = irc . serverdata . get ( ' separator ' ) or \
conf . conf . get ( ' relay ' , { } ) . get ( ' separator ' ) or " / "
2016-07-27 01:01:42 +02:00
# Figure out whether we tag nicks or not.
if times_tagged == 0 :
if conf . conf . get ( ' relay ' , { } ) . get ( ' tag_nicks ' , True ) :
times_tagged = 1
else :
forcetag_nicks = conf . conf . get ( ' relay ' , { } ) . get ( ' forcetag_nicks ' , [ ] )
log . debug ( ' ( %s ) relay.normalizeNick: checking if globs %s match %s . ' , irc . name , forcetag_nicks , nick )
for glob in forcetag_nicks :
if irc . matchHost ( glob , nick ) :
# User matched a nick to force tag nicks for. Tag them.
times_tagged = 1
break
2016-07-11 08:32:04 +02:00
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.normalizeNick: using %r as separator. ' , irc . name , separator )
2015-07-12 23:02:17 +02:00
orig_nick = nick
2015-09-06 03:00:57 +02:00
protoname = irc . protoname
2015-07-12 23:02:17 +02:00
maxnicklen = irc . maxnicklen
2016-06-19 21:30:34 +02:00
# Charybdis, IRCu, etc. don't allow / in nicks, and will SQUIT with a protocol
# violation if it sees one. Or it might just ignore the client introduction and
# cause bad desyncs.
2016-07-18 08:10:49 +02:00
protocol_allows_slashes = protoname . startswith ( ( ' insp ' , ' unreal ' , ' clientbot ' ) ) or \
2016-06-19 21:30:34 +02:00
irc . serverdata . get ( ' relay_force_slashes ' )
if ' / ' not in separator or not protocol_allows_slashes :
2016-07-02 04:16:47 +02:00
separator = separator . replace ( ' / ' , fallback_separator )
nick = nick . replace ( ' / ' , fallback_separator )
2016-06-19 21:30:34 +02:00
2016-07-02 04:19:11 +02:00
if nick . startswith ( tuple ( string . digits + ' - ' ) ) :
2015-08-06 06:56:52 +02:00
# On TS6 IRCds, nicks that start with 0-9 are only allowed if
2015-07-13 01:59:49 +02:00
# they match the UID of the originating server. Otherwise, you'll
2015-08-23 05:51:50 +02:00
# get nasty protocol violation SQUITs!
2016-07-02 04:19:11 +02:00
# Nicks starting with - are likewise not valid.
2015-07-13 01:59:49 +02:00
nick = ' _ ' + nick
2015-07-12 23:02:17 +02:00
2016-07-11 08:32:04 +02:00
# Maximum allowed length that relay nicks may have, minus the /network tag if used.
allowedlength = maxnicklen
# Track how many times the given nick has been tagged. If this is 0, no tag is used.
# If this is 1, a /network tag is added. Otherwise, keep adding one character to the
# separator: GLolol -> GLolol/net1 -> GLolol//net1 -> ...
if times_tagged > = 1 :
suffix = " %s %s %s " % ( separator [ 0 ] * times_tagged , separator [ 1 : ] , netname )
allowedlength - = len ( suffix )
2015-07-12 23:02:17 +02:00
2015-08-23 05:51:50 +02:00
# If a nick is too long, the real nick portion will be cut off, but the
# /network suffix MUST remain the same.
2015-07-12 23:02:17 +02:00
nick = nick [ : allowedlength ]
2016-07-11 08:32:04 +02:00
if times_tagged > = 1 :
nick + = suffix
2015-08-23 05:51:50 +02:00
2016-07-02 04:16:47 +02:00
# Loop over every character in the nick, making sure that it only contains valid
# characters.
for char in nick :
if char not in allowed_chars :
nick = nick . replace ( char , fallback_separator )
2016-01-01 02:28:47 +01:00
while irc . nickToUid ( nick ) and irc . nickToUid ( nick ) != uid :
2016-08-13 19:58:40 +02:00
# The nick we want exists: Increase the separator length by 1 if the user was already
# tagged, but couldn't be created due to a nick conflict. This can happen when someone
# steals a relay user's nick.
2016-08-13 20:40:01 +02:00
# However, if a user is changing from, say, a long, cut-off nick to another long, cut-off
2016-08-13 19:58:40 +02:00
# nick, we would skip tagging the nick twice if they originate from the same UID.
2016-07-11 08:32:04 +02:00
times_tagged + = 1
log . debug ( ' ( %s ) relay.normalizeNick: nick %r is in use; incrementing times tagged to %s . ' ,
irc . name , nick , times_tagged )
nick = normalizeNick ( irc , netname , orig_nick , times_tagged = times_tagged , uid = uid )
2016-07-02 04:16:47 +02:00
2015-07-12 23:02:17 +02:00
finalLength = len ( nick )
assert finalLength < = maxnicklen , " Normalized nick %r went over max " \
2015-09-10 05:53:04 +02:00
" nick length (got: %s , allowed: %s !) " % ( nick , finalLength , maxnicklen )
2015-07-12 23:02:17 +02:00
return nick
2015-11-22 08:37:19 +01:00
def normalizeHost ( irc , host ) :
""" Creates a normalized hostname for the given host suitable for
introduction to a remote network ( as a relay client ) . """
2016-08-22 03:06:53 +02:00
log . debug ( ' ( %s ) relay.normalizeHost: IRCd= %s , host= %s ' , irc . name , irc . protoname , host )
2016-08-08 00:16:37 +02:00
if irc . protoname not in ( ' inspircd ' , ' ts6 ' , ' clientbot ' , ' nefarious ' ) :
# UnrealIRCd and IRCd-Hybrid don't allow slashes in hostnames
2015-11-22 08:37:19 +01:00
host = host . replace ( ' / ' , ' . ' )
2016-09-20 15:58:04 +02:00
host = host . replace ( ' \x03 ' , ' ' ) # Strip colours
host = host . replace ( ' \x02 ' , ' ' ) # Strip bold
host = host . replace ( ' \x1f ' , ' ' ) # Strip underline
host = host . replace ( ' \x1d ' , ' ' ) # Strip italic
host = host . replace ( ' \x0f ' , ' ' ) # Strip color reset
host = host . replace ( ' \x16 ' , ' ' ) # Strip reverse color
# And no, I'm not supporting colours in hosts. Screw your IRCd-breaking patches if you think
# this is cool. -GL
2016-08-03 09:21:30 +02:00
return host [ : 63 ] # Limit hosts to 63 chars for best compatibility
2015-11-22 08:37:19 +01:00
2015-07-11 05:26:46 +02:00
def loadDB ( ) :
2015-09-15 02:23:56 +02:00
""" Loads the relay database, creating a new one if this fails. """
2015-07-11 05:26:46 +02:00
global db
try :
with open ( dbname , " rb " ) as f :
db = pickle . load ( f )
2016-04-02 03:31:53 +02:00
except ( ValueError , IOError , OSError ) :
log . info ( " Relay: failed to load links database %s "
" , creating a new one in memory... " , dbname )
2015-07-11 05:26:46 +02:00
db = { }
2016-02-28 03:13:26 +01:00
def exportDB ( ) :
""" Exports the relay database. """
log . debug ( " Relay: exporting links database to %s " , dbname )
with open ( dbname , ' wb ' ) as f :
pickle . dump ( db , f , protocol = 4 )
def scheduleExport ( starting = False ) :
"""
Schedules exporting of the relay database in a repeated loop .
"""
global exportdb_timer
if not starting :
2016-03-31 06:22:18 +02:00
# Export the database, unless this is being called the first
2016-02-28 03:13:26 +01:00
# thing after start (i.e. DB has just been loaded).
exportDB ( )
2016-07-08 07:16:21 +02:00
exportdb_timer = threading . Timer ( save_delay , scheduleExport )
2016-02-28 03:13:26 +01:00
exportdb_timer . name = ' PyLink Relay exportDB Loop '
exportdb_timer . start ( )
2015-07-11 05:26:46 +02:00
2015-09-13 22:48:14 +02:00
def getPrefixModes ( irc , remoteirc , channel , user , mlist = None ) :
"""
Fetches all prefix modes for a user in a channel that are supported by the
remote IRC network given .
Optionally , an mlist argument can be given to look at an earlier state of
2016-02-28 03:36:20 +01:00
the channel , e . g . for checking the op status of a mode setter before their
2015-09-13 22:48:14 +02:00
modes are processed and added to the channel state .
"""
2015-07-14 06:46:05 +02:00
modes = ' '
2016-02-28 03:36:20 +01:00
2016-03-20 01:54:42 +01:00
if user in irc . channels [ channel ] . users :
2016-08-10 03:39:13 +02:00
# Iterate over the the prefix modes for relay supported by the remote IRCd.
# Note: reverse the order so prefix modes are bursted in their traditional order
# (e.g. owner before op before halfop). TODO: SJOIN modes should probably be
# consistently sorted IRCd-side.
for pmode in reversed ( irc . channels [ channel ] . getPrefixModes ( user , prefixmodes = mlist ) ) :
2016-03-20 01:54:42 +01:00
if pmode in remoteirc . cmodes :
2015-07-23 04:29:58 +02:00
modes + = remoteirc . cmodes [ pmode ]
2015-07-14 06:46:05 +02:00
return modes
2016-05-01 08:59:51 +02:00
def spawnRelayServer ( irc , remoteirc ) :
2016-06-30 03:12:50 +02:00
irc . connected . wait ( 5 )
2016-05-01 08:59:51 +02:00
try :
# ENDBURST is delayed by 3 secs on supported IRCds to prevent
# triggering join-flood protection and the like.
sid = irc . proto . spawnServer ( ' %s .relay ' % remoteirc . name ,
desc = " PyLink Relay network - %s " %
( remoteirc . serverdata . get ( ' netname ' ) \
or remoteirc . name ) , endburst_delay = 3 )
except ValueError : # Network not initialized yet, or a server name conflict.
2016-07-14 03:56:55 +02:00
log . exception ( ' ( %s ) Failed to spawn server for %r (possible jupe?): ' ,
2016-05-01 08:59:51 +02:00
irc . name , remoteirc . name )
2016-07-14 03:43:22 +02:00
# We will just bail here. Disconnect the bad network.
2016-08-01 05:36:27 +02:00
try :
control . remove_network ( irc )
except KeyError :
pass
2016-07-14 03:49:51 +02:00
return
2016-05-01 08:59:51 +02:00
# Mark the server as a relay server
irc . servers [ sid ] . remote = remoteirc . name
# Assign the newly spawned server as our relay server for the target net.
relayservers [ irc . name ] [ remoteirc . name ] = sid
return sid
2015-09-12 21:06:58 +02:00
def getRemoteSid ( irc , remoteirc ) :
2015-09-15 02:23:56 +02:00
""" Gets the remote server SID representing remoteirc on irc, spawning
2015-09-12 21:06:58 +02:00
it if it doesn ' t exist. " " "
2015-11-16 06:42:58 +01:00
2016-05-01 23:51:18 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks_servers[ %s ] ' , irc . name , irc . name )
2016-07-02 03:54:35 +02:00
if spawnlocks_servers [ irc . name ] . acquire ( 5 ) :
2015-09-12 21:06:58 +02:00
try :
sid = relayservers [ irc . name ] [ remoteirc . name ]
except KeyError :
2016-05-01 08:59:51 +02:00
log . debug ( ' ( %s ) getRemoteSid: %s .relay doesn \' t have a known SID, spawning. ' , irc . name , remoteirc . name )
sid = spawnRelayServer ( irc , remoteirc )
log . debug ( ' ( %s ) getRemoteSid: got %s for %s .relay ' , irc . name , sid , remoteirc . name )
if sid not in irc . servers :
log . debug ( ' ( %s ) getRemoteSid: SID %s for %s .relay doesn \' t exist, respawning ' , irc . name , sid , remoteirc . name )
# Our stored server doesn't exist anymore. This state is probably a holdover from a netsplit,
# so let's refresh it.
sid = spawnRelayServer ( irc , remoteirc )
elif sid in irc . servers and irc . servers [ sid ] . remote != remoteirc . name :
log . debug ( ' ( %s ) getRemoteSid: %s .relay != %s .relay, respawning ' , irc . name , irc . servers [ sid ] . remote , remoteirc . name )
sid = spawnRelayServer ( irc , remoteirc )
log . debug ( ' ( %s ) getRemoteSid: got %s for %s .relay (round 2) ' , irc . name , sid , remoteirc . name )
2016-07-02 03:54:35 +02:00
spawnlocks_servers [ irc . name ] . release ( )
2015-09-12 21:06:58 +02:00
return sid
2016-07-11 08:48:10 +02:00
def spawnRelayUser ( irc , remoteirc , user , times_tagged = 0 ) :
2016-05-01 08:59:51 +02:00
userobj = irc . users . get ( user )
if userobj is None :
# The query wasn't actually a valid user, or the network hasn't
# been connected yet... Oh well!
return
2016-07-11 08:48:10 +02:00
nick = normalizeNick ( remoteirc , irc . name , userobj . nick , times_tagged = times_tagged )
2016-05-01 08:59:51 +02:00
# Truncate idents at 10 characters, because TS6 won't like them otherwise!
ident = userobj . ident [ : 10 ]
# Normalize hostnames
host = normalizeHost ( remoteirc , userobj . host )
realname = userobj . realname
modes = set ( getSupportedUmodes ( irc , remoteirc , userobj . modes ) )
opertype = ' '
if ( ' o ' , None ) in userobj . modes :
if hasattr ( userobj , ' opertype ' ) :
# InspIRCd's special OPERTYPE command; this is mandatory
# and setting of umode +/-o will fail unless this
# is used instead. This also sets an oper type for
# the user, which is used in WHOIS, etc.
# If an opertype exists for the user, add " (remote)"
# for the relayed clone, so that it shows in whois.
# Janus does this too. :)
log . debug ( ' ( %s ) relay.getRemoteUser: setting OPERTYPE of client for %r to %s ' ,
irc . name , user , userobj . opertype )
opertype = userobj . opertype + ' (remote) '
else :
opertype = ' IRC Operator (remote) '
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc . umodes . get ( ' hideoper ' )
try :
2016-07-28 02:02:04 +02:00
use_hideoper = conf . conf [ ' relay ' ] [ ' hideoper ' ]
2016-05-01 08:59:51 +02:00
except KeyError :
use_hideoper = True
if hideoper_mode and use_hideoper :
modes . add ( ( hideoper_mode , None ) )
rsid = getRemoteSid ( remoteirc , irc )
try :
2016-07-28 02:02:04 +02:00
showRealIP = conf . conf [ ' relay ' ] [ ' show_ips ' ] and not \
2016-05-01 08:59:51 +02:00
irc . serverdata . get ( ' relay_no_ips ' ) and not \
remoteirc . serverdata . get ( ' relay_no_ips ' )
except KeyError :
showRealIP = False
if showRealIP :
ip = userobj . ip
realhost = userobj . realhost
else :
realhost = None
ip = ' 0.0.0.0 '
2016-07-11 07:25:01 +02:00
u = remoteirc . proto . spawnClient ( nick , ident = ident , host = host , realname = realname , modes = modes ,
opertype = opertype , server = rsid , ip = ip , realhost = realhost ) . uid
2016-07-02 03:54:07 +02:00
try :
remoteirc . users [ u ] . remote = ( irc . name , user )
remoteirc . users [ u ] . opertype = opertype
away = userobj . away
if away :
remoteirc . proto . away ( u , away )
except KeyError :
# User got killed somehow while we were setting options on it.
# This is probably being done by the uplink, due to something like an
# invalid nick, etc.
raise
2016-05-01 08:59:51 +02:00
relayusers [ ( irc . name , user ) ] [ remoteirc . name ] = u
return u
2016-07-11 08:48:10 +02:00
def getRemoteUser ( irc , remoteirc , user , spawnIfMissing = True , times_tagged = 0 ) :
2016-05-01 23:21:14 +02:00
"""
Gets the UID of the relay client requested on the target network ( remoteirc ) ,
2015-09-15 02:23:56 +02:00
spawning one if it doesn ' t exist and spawnIfMissing is True. " " "
2016-05-01 08:59:51 +02:00
2016-06-25 19:43:07 +02:00
# Wait until the network is working before trying to spawn anything.
2016-06-30 03:12:50 +02:00
irc . connected . wait ( 5 )
2016-05-01 08:59:51 +02:00
2016-05-15 01:17:20 +02:00
# Don't spawn clones for registered service bots.
sbot = irc . isServiceBot ( user )
if sbot :
return sbot . uids . get ( remoteirc . name )
2016-05-01 23:21:14 +02:00
2016-05-01 23:51:18 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] ' , irc . name , irc . name )
2016-07-02 03:54:35 +02:00
if spawnlocks [ irc . name ] . acquire ( 5 ) :
2016-05-01 23:21:14 +02:00
# Be sort-of thread safe: lock the user spawns for the current net first.
2016-05-01 08:59:51 +02:00
u = None
2015-08-14 17:52:09 +02:00
try :
2016-05-01 23:21:14 +02:00
# Look up the existing user, stored here as dict entries in the format:
# {('ournet', 'UID'): {'remotenet1': 'UID1', 'remotenet2': 'UID2'}}
2015-08-14 17:52:09 +02:00
u = relayusers [ ( irc . name , user ) ] [ remoteirc . name ]
except KeyError :
2016-05-01 23:21:14 +02:00
# User doesn't exist. Spawn a new one if requested.
2016-05-01 08:59:51 +02:00
if spawnIfMissing :
2016-07-11 08:48:10 +02:00
u = spawnRelayUser ( irc , remoteirc , user , times_tagged = times_tagged )
2015-11-22 22:08:31 +01:00
2016-05-01 23:21:14 +02:00
# This is a sanity check to make sure netsplits and other state resets
# don't break the relayer. If it turns out there was a client in our relayusers
# cache for the requested UID, but it doesn't match the request,
# assume it was a leftover from the last split and replace it with a new one.
2016-05-01 08:59:51 +02:00
if u and ( ( u not in remoteirc . users ) or remoteirc . users [ u ] . remote != ( irc . name , user ) ) :
2016-07-11 08:48:10 +02:00
u = spawnRelayUser ( irc , remoteirc , user , times_tagged = times_tagged )
2016-07-02 03:54:35 +02:00
spawnlocks [ irc . name ] . release ( )
2015-08-14 17:52:09 +02:00
return u
2015-07-14 21:04:05 +02:00
2015-09-15 02:29:37 +02:00
def getOrigUser ( irc , user , targetirc = None ) :
2015-09-15 02:23:56 +02:00
"""
Given the UID of a relay client , returns a tuple of the home network name
and original UID of the user it was spawned for .
2015-07-22 08:47:06 +02:00
2015-09-15 02:23:56 +02:00
If targetirc is given , getRemoteUser ( ) is called to get the relay client
representing the original user on that target network . """
2016-09-07 04:53:21 +02:00
2015-08-16 04:18:04 +02:00
try :
remoteuser = irc . users [ user ] . remote
except ( AttributeError , KeyError ) :
remoteuser = None
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.getOrigUser: remoteuser set to %r (looking up %s / %s ). ' ,
2015-09-15 02:23:56 +02:00
irc . name , remoteuser , user , irc . name )
2015-07-22 08:47:06 +02:00
if remoteuser :
# If targetirc is given, we'll return simply the UID of the user on the
# target network, if it exists. Otherwise, we'll return a tuple
# with the home network name and the original user's UID.
2015-08-29 18:39:33 +02:00
sourceobj = world . networkobjects . get ( remoteuser [ 0 ] )
2015-07-22 08:47:06 +02:00
if targetirc and sourceobj :
if remoteuser [ 0 ] == targetirc . name :
# The user we found's home network happens to be the one being
# requested; just return the UID then.
return remoteuser [ 1 ]
# Otherwise, use getRemoteUser to find our UID.
2015-09-15 02:23:56 +02:00
res = getRemoteUser ( sourceobj , targetirc , remoteuser [ 1 ] ,
spawnIfMissing = False )
2016-07-08 07:16:34 +02:00
log . debug ( ' ( %s ) relay.getOrigUser: targetirc found as %s , getting %r as '
' remoteuser for %r (looking up %s / %s ). ' , irc . name , targetirc . name ,
res , remoteuser [ 1 ] , user , irc . name )
2015-07-22 08:47:06 +02:00
return res
else :
2015-07-15 04:39:49 +02:00
return remoteuser
2015-09-15 02:29:37 +02:00
def getRelay ( chanpair ) :
2015-09-17 05:59:08 +02:00
""" Finds the matching relay entry name for the given (network name, channel)
pair , if one exists . """
2015-07-13 08:28:54 +02:00
if chanpair in db : # This chanpair is a shared channel; others link to it
return chanpair
# This chanpair is linked *to* a remote channel
for name , dbentry in db . items ( ) :
if chanpair in dbentry [ ' links ' ] :
return name
2015-07-13 04:03:18 +02:00
2015-09-15 02:29:37 +02:00
def getRemoteChan ( irc , remoteirc , channel ) :
2015-09-15 02:23:56 +02:00
""" Returns the linked channel name for the given channel on remoteirc,
if one exists . """
2015-07-14 21:04:05 +02:00
query = ( irc . name , channel )
remotenetname = remoteirc . name
2015-09-15 02:29:37 +02:00
chanpair = getRelay ( query )
2015-07-14 04:46:24 +02:00
if chanpair is None :
return
if chanpair [ 0 ] == remotenetname :
return chanpair [ 1 ]
else :
for link in db [ chanpair ] [ ' links ' ] :
if link [ 0 ] == remotenetname :
return link [ 1 ]
2015-07-13 09:01:04 +02:00
def initializeChannel ( irc , channel ) :
2015-09-15 02:23:56 +02:00
""" Initializes a relay channel (merge local/remote users, set modes, etc.). """
2015-07-14 06:46:05 +02:00
# We're initializing a relay that already exists. This can be done at
# ENDBURST, or on the LINK command.
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel being called on %s ' , irc . name , channel )
log . debug ( ' ( %s ) relay.initializeChannel: relay pair found to be %s ' , irc . name , relay )
2015-07-14 21:04:05 +02:00
queued_users = [ ]
2015-07-14 06:46:05 +02:00
if relay :
all_links = db [ relay ] [ ' links ' ] . copy ( )
all_links . update ( ( relay , ) )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel: all_links: %s ' , irc . name , all_links )
2015-07-23 04:29:58 +02:00
# Iterate over all the remote channels linked in this relay.
2015-07-14 06:46:05 +02:00
for link in all_links :
2015-07-13 09:01:04 +02:00
remotenet , remotechan = link
2016-02-28 03:36:20 +01:00
if remotenet == irc . name : # If the network is us, skip.
2015-07-14 06:46:05 +02:00
continue
2015-08-29 18:39:33 +02:00
remoteirc = world . networkobjects . get ( remotenet )
2016-02-28 03:36:20 +01:00
2015-07-21 08:36:26 +02:00
if remoteirc is None :
2016-02-28 03:36:20 +01:00
# Remote network doesn't have an IRC object; e.g. it was removed
# from the config. Skip this.
2015-07-21 08:36:26 +02:00
continue
2015-07-14 06:46:05 +02:00
rc = remoteirc . channels [ remotechan ]
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
if not ( remoteirc . connected . is_set ( ) and getRemoteChan ( remoteirc , irc , remotechan ) ) :
2016-02-28 03:36:20 +01:00
continue # Remote network isn't connected.
2015-07-23 04:29:58 +02:00
# Join their (remote) users and set their modes.
2015-08-16 08:31:54 +02:00
relayJoins ( remoteirc , remotechan , rc . users , rc . ts )
2015-08-16 08:05:09 +02:00
topic = remoteirc . channels [ remotechan ] . topic
2016-02-28 03:36:20 +01:00
2015-07-20 22:18:04 +02:00
# Only update the topic if it's different from what we already have,
# and topic bursting is complete.
2015-08-16 08:05:09 +02:00
if remoteirc . channels [ remotechan ] . topicset and topic != irc . channels [ channel ] . topic :
2016-05-03 03:41:51 +02:00
irc . proto . topicBurst ( irc . sid , channel , topic )
2016-02-28 03:36:20 +01:00
2015-09-03 02:41:49 +02:00
# Send our users and channel modes to the other nets
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel: joining our ( %s ) users: %s ' , irc . name , remotenet , irc . channels [ channel ] . users )
2015-09-12 21:06:58 +02:00
relayJoins ( irc , channel , irc . channels [ channel ] . users , irc . channels [ channel ] . ts )
2016-06-25 23:21:18 +02:00
world . services [ ' pylink ' ] . extra_channels [ irc . name ] . add ( channel )
2016-04-18 20:52:01 +02:00
if irc . pseudoclient and irc . pseudoclient . uid not in irc . channels [ channel ] . users :
2016-01-17 01:36:45 +01:00
irc . proto . join ( irc . pseudoclient . uid , channel )
2015-07-14 21:04:05 +02:00
2015-09-15 02:23:56 +02:00
def removeChannel ( irc , channel ) :
""" Destroys a relay channel by parting all of its users. """
if irc is None :
2015-07-29 13:02:45 +02:00
return
2016-03-05 18:31:59 +01:00
2016-06-28 08:00:39 +02:00
if channel not in map ( str . lower , irc . serverdata . get ( ' channels ' , [ ] ) ) :
2016-06-25 23:21:18 +02:00
world . services [ ' pylink ' ] . extra_channels [ irc . name ] . discard ( channel )
2016-01-17 01:51:54 +01:00
irc . proto . part ( irc . pseudoclient . uid , channel , ' Channel delinked. ' )
2016-03-05 18:31:59 +01:00
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-09-15 02:23:56 +02:00
if relay :
for user in irc . channels [ channel ] . users . copy ( ) :
if not isRelayClient ( irc , user ) :
relayPart ( irc , channel , user )
# Don't ever part the main client from any of its autojoin channels.
2015-08-15 09:02:46 +02:00
else :
2015-09-15 02:23:56 +02:00
if user == irc . pseudoclient . uid and channel in \
2016-06-28 08:00:39 +02:00
irc . serverdata . get ( ' channels ' , [ ] ) :
2015-09-15 02:23:56 +02:00
continue
2016-01-17 01:51:54 +01:00
irc . proto . part ( user , channel , ' Channel delinked. ' )
2015-09-15 02:23:56 +02:00
# Don't ever quit it either...
if user != irc . pseudoclient . uid and not irc . users [ user ] . channels :
2015-09-15 02:29:37 +02:00
remoteuser = getOrigUser ( irc , user )
2015-09-15 02:23:56 +02:00
del relayusers [ remoteuser ] [ irc . name ]
2016-01-17 01:51:54 +01:00
irc . proto . quit ( user , ' Left all shared channels. ' )
2015-07-15 04:39:49 +02:00
2015-09-13 22:48:14 +02:00
def checkClaim ( irc , channel , sender , chanobj = None ) :
2015-09-13 07:50:53 +02:00
"""
Checks whether the sender of a kick / mode change passes CLAIM checks for
a given channel . This returns True if any of the following criteria are met :
2015-09-17 05:59:08 +02:00
1 ) No relay exists for the channel in question .
2 ) The originating network is the one that created the relay .
3 ) The CLAIM list for the relay in question is empty .
4 ) The originating network is in the CLAIM list for the relay in question .
5 ) The sender is halfop or above in the channel .
6 ) The sender is a PyLink client / server ( checks are suppressed in this case ) .
2015-09-13 07:50:53 +02:00
"""
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-09-13 22:48:14 +02:00
try :
mlist = chanobj . prefixmodes
except AttributeError :
mlist = None
2016-03-20 01:54:42 +01:00
2015-09-13 22:48:14 +02:00
sender_modes = getPrefixModes ( irc , irc , channel , sender , mlist = mlist )
log . debug ( ' ( %s ) relay.checkClaim: sender modes ( %s / %s ) are %s (mlist= %s ) ' , irc . name ,
sender , channel , sender_modes , mlist )
2016-03-20 01:54:42 +01:00
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
2015-09-17 05:59:08 +02:00
return ( not relay ) or irc . name == relay [ 0 ] or not db [ relay ] [ ' claim ' ] or \
irc . name in db [ relay ] [ ' claim ' ] or \
2015-09-13 07:50:53 +02:00
any ( [ mode in sender_modes for mode in ( ' y ' , ' q ' , ' a ' , ' o ' , ' h ' ) ] ) \
2016-01-01 02:28:47 +01:00
or irc . isInternalClient ( sender ) or \
irc . isInternalServer ( sender )
2015-09-13 07:50:53 +02:00
2015-09-15 02:23:56 +02:00
def getSupportedUmodes ( irc , remoteirc , modes ) :
""" Given a list of user modes, filters out all of those not supported by the
remote network . """
supported_modes = [ ]
2016-02-28 03:36:20 +01:00
# Iterate over all mode pairs.
2015-09-15 02:23:56 +02:00
for modepair in modes :
try :
2016-02-28 03:36:20 +01:00
# Get the prefix and the actual mode character (the prefix being + or -, or
# whether we're setting or unsetting a mode)
2015-09-15 02:23:56 +02:00
prefix , modechar = modepair [ 0 ]
except ValueError :
2016-02-28 03:36:20 +01:00
# If the prefix is missing, assume we're adding a mode.
2015-09-15 02:23:56 +02:00
modechar = modepair [ 0 ]
prefix = ' + '
2016-02-28 03:36:20 +01:00
# Get the mode argument.
2015-09-15 02:23:56 +02:00
arg = modepair [ 1 ]
2016-02-28 03:36:20 +01:00
# Iterate over all supported user modes for the current network.
2015-09-15 02:23:56 +02:00
for name , m in irc . umodes . items ( ) :
2016-09-08 05:37:57 +02:00
if name . startswith ( ' * ' ) :
# XXX: Okay, we need a better place to store modetypes.
continue
2015-09-15 02:23:56 +02:00
supported_char = None
2016-02-28 03:36:20 +01:00
# Mode character matches one in our list, so set that named mode
# as the one we're trying to set. Then, look up that named mode
# in the supported modes list for the TARGET network, and set that
# mode character as the one we're setting, if it exists.
2015-09-15 02:23:56 +02:00
if modechar == m :
if name not in whitelisted_umodes :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.getSupportedUmodes: skipping mode ( %r , %r ) because "
2015-09-15 02:23:56 +02:00
" it isn ' t a whitelisted (safe) mode for relay. " ,
irc . name , modechar , arg )
break
supported_char = remoteirc . umodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if supported_char :
supported_modes . append ( ( prefix + supported_char , arg ) )
break
else :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.getSupportedUmodes: skipping mode ( %r , %r ) because "
2015-09-15 02:23:56 +02:00
" the remote network ( %s ) ' s IRCd ( %s ) doesn ' t support it. " ,
irc . name , modechar , arg , remoteirc . name ,
remoteirc . protoname )
return supported_modes
def isRelayClient ( irc , user ) :
""" Returns whether the given user is a relay client. """
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 AttributeError : # Nope, it isn't.
pass
except KeyError : # The user doesn't exist?!?
return True
return False
### EVENT HANDLER INTERNALS
def relayJoins ( irc , channel , users , ts , burst = True ) :
2016-02-28 03:36:20 +01:00
"""
Relays one or more users ' joins from a channel to its relay links.
"""
2016-08-13 03:46:39 +02:00
joined_nets = { }
2015-11-27 07:50:20 +01:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
queued_users = [ ]
if name == irc . name or not remoteirc . connected . is_set ( ) :
# Don't relay things to their source network...
2015-07-15 03:20:20 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-07-15 03:20:20 +02:00
if remotechan is None :
2016-02-28 03:36:20 +01:00
# If there is no link on the current network for the channel in question,
# just skip it
2015-07-15 03:20:20 +02:00
continue
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayJoins: got %r for users ' , irc . name , users )
2015-09-15 02:23:56 +02:00
for user in users . copy ( ) :
if isRelayClient ( irc , user ) :
2016-02-28 03:36:20 +01:00
# Don't clone relay clients; that'll cause bad infinite loops.
2015-09-15 02:23:56 +02:00
continue
2016-02-28 03:36:20 +01:00
assert user in irc . users , " ( %s ) relay.relayJoins: How is this possible? %r isn ' t in our user database. " % ( irc . name , user )
2015-09-15 02:23:56 +02:00
u = getRemoteUser ( irc , remoteirc , user )
2016-02-28 03:36:20 +01:00
2016-05-14 23:03:59 +02:00
if not u :
continue
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if u not in remoteirc . channels [ remotechan ] . users :
2016-02-28 03:36:20 +01:00
# Note: only join users if they aren't already joined. This prevents op floods
# on charybdis from repeated SJOINs sent for one user.
# Fetch the known channel TS and all the prefix modes for each user. This ensures
# the different sides of the relay are merged properly.
2015-09-15 02:23:56 +02:00
ts = irc . channels [ channel ] . ts
prefixes = getPrefixModes ( irc , remoteirc , channel , user )
2016-02-28 03:36:20 +01:00
# proto.sjoin() takes its users as a list of (prefix mode characters, UID) pairs.
2015-09-15 02:23:56 +02:00
userpair = ( prefixes , u )
queued_users . append ( userpair )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if queued_users :
2016-02-28 03:36:20 +01:00
# Look at whether we should relay this join as a regular JOIN, or a SJOIN.
# SJOIN will be used if either the amount of users to join is > 1, or there are modes
# to be set on the joining user.
2015-09-15 02:23:56 +02:00
if burst or len ( queued_users ) > 1 or queued_users [ 0 ] [ 0 ] :
2016-06-23 04:49:49 +02:00
modes = getSupportedCmodes ( irc , remoteirc , channel , irc . channels [ channel ] . modes )
2016-06-23 06:41:04 +02:00
remoteirc . proto . sjoin ( getRemoteSid ( remoteirc , irc ) , remotechan , queued_users , ts = ts , modes = modes )
2015-09-15 02:23:56 +02:00
else :
2016-02-28 03:36:20 +01:00
# A regular JOIN only needs the user and the channel. TS, source SID, etc., can all be omitted.
2016-01-17 01:36:45 +01:00
remoteirc . proto . join ( queued_users [ 0 ] [ 1 ] , remotechan )
2015-07-20 08:49:50 +02:00
2016-08-13 03:46:39 +02:00
joined_nets [ remoteirc ] = { ' channel ' : remotechan , ' users ' : [ u [ - 1 ] for u in queued_users ] }
for remoteirc , hookdata in joined_nets . items ( ) :
# HACK: Announce this JOIN as a special hook on each network, for plugins like Automode.
remoteirc . callHooks ( [ remoteirc . sid , ' PYLINK_RELAY_JOIN ' , hookdata ] )
2016-07-08 06:38:12 +02:00
2015-09-15 02:23:56 +02:00
def relayPart ( irc , channel , user ) :
2016-02-28 03:36:20 +01:00
"""
Relays a user part from a channel to its relay links , as part of a channel delink .
"""
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
if name == irc . name or not remoteirc . connected . is_set ( ) :
# Don't relay things to their source network...
2015-08-18 11:44:27 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayPart: looking for %s / %s on %s ' , irc . name , user , irc . name , remoteirc . name )
log . debug ( ' ( %s ) relay.relayPart: remotechan found as %s ' , irc . name , remotechan )
2015-09-15 02:23:56 +02:00
remoteuser = getRemoteUser ( irc , remoteirc , user , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayPart: remoteuser for %s / %s found as %s ' , irc . name , user , irc . name , remoteuser )
2015-09-15 02:23:56 +02:00
if remotechan is None or remoteuser is None :
2016-02-28 03:36:20 +01:00
# If there is no relay channel on the target network, or the relay
# user doesn't exist, just do nothing.
2015-09-15 02:23:56 +02:00
continue
2016-02-28 03:36:20 +01:00
# Part the relay client with the channel delinked message.
2016-01-17 01:51:54 +01:00
remoteirc . proto . part ( remoteuser , remotechan , ' Channel delinked. ' )
2016-02-28 03:36:20 +01:00
# If the relay client no longer has any channels, quit them to prevent inflating /lusers.
2015-09-15 02:23:56 +02:00
if isRelayClient ( remoteirc , remoteuser ) and not remoteirc . users [ remoteuser ] . channels :
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( remoteuser , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , user ) ] [ remoteirc . name ]
2015-07-16 20:53:40 +02:00
2015-07-20 00:56:29 +02:00
whitelisted_cmodes = { ' admin ' , ' allowinvite ' , ' autoop ' , ' ban ' , ' banexception ' ,
' blockcolor ' , ' halfop ' , ' invex ' , ' inviteonly ' , ' key ' ,
' limit ' , ' moderated ' , ' noctcp ' , ' noextmsg ' , ' nokick ' ,
' noknock ' , ' nonick ' , ' nonotice ' , ' op ' , ' operonly ' ,
' opmoderated ' , ' owner ' , ' private ' , ' regonly ' ,
2015-07-25 05:55:48 +02:00
' regmoderated ' , ' secret ' , ' sslonly ' , ' adminonly ' ,
2016-05-12 06:18:30 +02:00
' stripcolor ' , ' topiclock ' , ' voice ' , ' flood ' ,
' flood_unreal ' , ' joinflood ' , ' freetarget ' ,
2016-05-15 20:05:02 +02:00
' noforwards ' , ' noinvite ' }
2015-07-20 07:43:26 +02:00
whitelisted_umodes = { ' bot ' , ' hidechans ' , ' hideoper ' , ' invisible ' , ' oper ' ,
2016-05-15 20:05:02 +02:00
' regdeaf ' , ' stripcolor ' , ' noctcp ' , ' wallops ' ,
2015-11-22 22:08:31 +01:00
' hideidle ' }
2016-06-23 04:49:49 +02:00
def getSupportedCmodes ( irc , remoteirc , channel , modes ) :
2016-02-28 03:36:20 +01:00
"""
2016-06-23 04:49:49 +02:00
Filters a channel mode change to the modes supported by the target IRCd .
2016-02-28 03:36:20 +01:00
"""
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-06-23 04:49:49 +02:00
if not remotechan : # Not a relay channel
return [ ]
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
supported_modes = [ ]
for modepair in modes :
try :
prefix , modechar = modepair [ 0 ]
except ValueError :
modechar = modepair [ 0 ]
prefix = ' + '
arg = modepair [ 1 ]
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
# Iterate over every mode see whether the remote IRCd supports
# this mode, and what its mode char for it is (if it is different).
for name , m in irc . cmodes . items ( ) :
supported_char = None
2016-09-08 05:37:57 +02:00
if name . startswith ( ' * ' ) :
# XXX: Okay, we need a better place to store modetypes.
continue
2015-07-15 04:39:49 +02:00
if modechar == m :
2016-09-08 05:37:57 +02:00
2015-07-22 08:53:29 +02:00
supported_char = remoteirc . cmodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-07-22 08:53:29 +02:00
if supported_char is None :
break
2016-02-28 03:36:20 +01:00
2015-07-20 00:56:29 +02:00
if name not in whitelisted_cmodes :
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: skipping mode ( %r , %r ) because "
2015-07-20 00:56:29 +02:00
" it isn ' t a whitelisted (safe) mode for relay. " ,
irc . name , modechar , arg )
break
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
if modechar in irc . prefixmodes :
# This is a prefix mode (e.g. +o). We must coerse the argument
# so that the target exists on the remote relay network.
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: coersing argument of ( %r , %r ) "
2015-07-22 07:14:53 +02:00
" for network %r . " ,
irc . name , modechar , arg , remoteirc . name )
2016-02-28 03:36:20 +01:00
2015-07-22 08:47:06 +02:00
# If the target is a remote user, get the real target
# (original user).
2015-09-15 02:29:37 +02:00
arg = getOrigUser ( irc , arg , targetirc = remoteirc ) or \
2015-07-22 08:47:06 +02:00
getRemoteUser ( irc , remoteirc , arg , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
2016-07-08 07:16:34 +02:00
if arg is None :
# Relay client for target user doesn't exist yet. Drop the mode.
break
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: argument found as ( %r , %r ) "
2015-07-22 08:47:06 +02:00
" for network %r . " ,
irc . name , modechar , arg , remoteirc . name )
2016-03-20 01:25:04 +01:00
oplist = remoteirc . channels [ remotechan ] . prefixmodes [ name ]
2016-02-28 03:36:20 +01:00
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: list of %s s on %r is: %s " ,
2015-07-25 19:43:47 +02:00
irc . name , name , remotechan , oplist )
2016-02-28 03:36:20 +01:00
2015-07-25 19:43:47 +02:00
if prefix == ' + ' and arg in oplist :
2015-07-22 08:53:29 +02:00
# Don't set prefix modes that are already set.
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: skipping setting %s on %s / %s because it appears to be already set. " ,
2015-07-22 08:53:29 +02:00
irc . name , name , arg , remoteirc . name )
break
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
supported_char = remoteirc . cmodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
if supported_char :
2015-07-25 19:43:47 +02:00
final_modepair = ( prefix + supported_char , arg )
2015-07-20 00:20:23 +02:00
if name in ( ' ban ' , ' banexception ' , ' invex ' ) and not utils . isHostmask ( arg ) :
# Don't add bans that don't match n!u@h syntax!
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: skipping mode ( %r , %r ) because it doesn ' t match nick!user@host syntax. " ,
2015-07-20 00:56:29 +02:00
irc . name , modechar , arg )
break
2016-02-28 03:36:20 +01:00
2015-07-22 08:53:29 +02:00
# Don't set modes that are already set, to prevent floods on TS6
# where the same mode can be set infinite times.
2015-07-25 19:43:47 +02:00
if prefix == ' + ' and final_modepair in remoteirc . channels [ remotechan ] . modes :
2016-06-23 04:49:49 +02:00
log . debug ( " ( %s ) relay.getSupportedCmodes: skipping setting mode ( %r , %r ) on %s %s because it appears to be already set. " ,
2015-07-22 08:53:29 +02:00
irc . name , supported_char , arg , remoteirc . name , remotechan )
break
2016-02-28 03:36:20 +01:00
2015-07-25 19:43:47 +02:00
supported_modes . append ( final_modepair )
2016-02-28 03:36:20 +01:00
2016-06-23 04:49:49 +02:00
log . debug ( ' ( %s ) relay.getSupportedCmodes: final modelist (sending to %s %s ) is %s ' , irc . name , remoteirc . name , remotechan , supported_modes )
return supported_modes
2015-07-15 04:39:49 +02:00
2016-05-20 07:23:34 +02:00
### EVENT HANDLERS
def handle_relay_whois ( irc , source , command , args ) :
2016-02-28 03:36:20 +01:00
"""
WHOIS handler for the relay plugin .
"""
2016-05-20 07:23:34 +02:00
target = args [ ' target ' ]
server = args [ ' server ' ]
targetuser = irc . users [ target ]
def wreply ( num , text ) :
""" Convenience wrapper to return WHOIS replies. """
# WHOIS replies are by convention prefixed with the target user's nick.
text = ' %s %s ' % ( targetuser . nick , text )
irc . proto . numeric ( server , num , source , text )
2016-05-20 08:30:34 +02:00
def checkSendKey ( infoline ) :
"""
Returns whether we should send the given info line in WHOIS . This validates the
corresponding configuration option for being either " all " or " opers " . """
2016-07-28 02:02:04 +02:00
setting = conf . conf . get ( ' relay ' , { } ) . get ( infoline , ' ' ) . lower ( )
2016-05-20 08:30:34 +02:00
if setting == ' all ' :
return True
elif setting == ' opers ' and irc . isOper ( source , allowAuthed = False ) :
return True
return False
2016-05-20 07:23:34 +02:00
# Get the real user for the WHOIS target.
origuser = getOrigUser ( irc , target )
if origuser :
homenet , uid = origuser
realirc = world . networkobjects [ homenet ]
realuser = realirc . users [ uid ]
netname = realirc . serverdata . get ( ' netname ' , homenet )
wreply ( 320 , " :is a remote user connected via PyLink Relay. Home network: %s ; "
" Home nick: %s " % ( netname , realuser . nick ) )
2016-05-20 08:30:34 +02:00
if checkSendKey ( ' whois_show_accounts ' ) and realuser . services_account :
# Send account information if told to and the target is logged in.
wreply ( 330 , " %s :is logged in (on %s ) as " % ( realuser . services_account , netname ) )
2016-07-24 07:30:10 +02:00
if checkSendKey ( ' whois_show_server ' ) and realirc . protoname != ' clientbot ' :
2016-05-20 08:30:34 +02:00
wreply ( 320 , " :is actually connected via the following server: " )
realserver = realirc . getServer ( uid )
realserver = realirc . servers [ realserver ]
wreply ( 312 , " %s : %s " % ( realserver . name , realserver . desc ) )
2016-05-20 07:23:34 +02:00
utils . add_hook ( handle_relay_whois , ' PYLINK_CUSTOM_WHOIS ' )
2015-09-15 02:23:56 +02:00
def handle_operup ( irc , numeric , command , args ) :
2016-07-02 06:07:07 +02:00
"""
Handles setting oper types on relay clients during oper up .
"""
newtype = args [ ' text ' ] + ' (remote) '
2015-09-15 02:23:56 +02:00
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
2015-09-20 20:25:45 +02:00
log . debug ( ' ( %s ) relay.handle_opertype: setting OPERTYPE of %s / %s to %s ' ,
irc . name , user , netname , newtype )
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ netname ]
remoteirc . users [ user ] . opertype = newtype
2015-12-27 00:41:22 +01:00
utils . add_hook ( handle_operup , ' CLIENT_OPERED ' )
2015-09-15 02:23:56 +02:00
def handle_join ( irc , numeric , command , args ) :
channel = args [ ' channel ' ]
2015-09-15 02:29:37 +02:00
if not getRelay ( ( irc . name , channel ) ) :
2015-09-15 02:23:56 +02:00
# No relay here, return.
return
ts = args [ ' ts ' ]
users = set ( args [ ' users ' ] )
2016-08-28 04:09:02 +02:00
claim_passed = checkClaim ( irc , channel , numeric )
current_chandata = irc . channels [ channel ]
2016-09-03 02:52:19 +02:00
chandata = args . get ( ' channeldata ' )
2016-08-28 04:09:02 +02:00
log . debug ( ' ( %s ) relay.handle_join: claim for %s on %s : %s ' , irc . name , numeric , channel , claim_passed )
log . debug ( ' ( %s ) relay.handle_join: old channel data %s ' , irc . name , chandata )
log . debug ( ' ( %s ) relay.handle_join: current channel data %s ' , irc . name , current_chandata )
if chandata and not claim_passed :
# If the server we're receiving an SJOIN from isn't in the claim list, undo ALL attempts
# from it to burst modes.
# This option can prevent things like /OJOIN abuse or split riding with oper override, but
# has the side effect of causing all prefix modes on leaf links to be lost when networks
# split and rejoin.
modes = [ ]
for user in users :
# XXX: Find the diff of the new and old mode lists of the channel. Not pretty, but I'd
# rather not change the 'users' format of SJOIN just for this. -GL
try :
oldmodes = set ( chandata . getPrefixModes ( user ) )
except KeyError :
# User was never in channel. Treat their mode list as empty.
oldmodes = set ( )
newmodes = set ( current_chandata . getPrefixModes ( user ) )
modediff = newmodes - oldmodes
log . debug ( ' ( %s ) relay.handle_join: mode diff for %s on %s : %s oldmodes= %s newmodes= %s ' ,
irc . name , user , channel , modediff , oldmodes , newmodes )
for modename in modediff :
modechar = irc . cmodes . get ( modename )
if modechar :
modes . append ( ( ' - %s ' % modechar , user ) )
if modes :
log . debug ( ' ( %s ) relay.handle_join: reverting modes on BURST: %s ' , irc . name , irc . joinModes ( modes ) )
irc . proto . mode ( irc . sid , channel , modes )
2015-09-15 02:23:56 +02:00
relayJoins ( irc , channel , users , ts , burst = False )
utils . add_hook ( handle_join , ' JOIN ' )
def handle_quit ( irc , numeric , command , args ) :
2016-05-01 23:21:50 +02:00
# Lock the user spawning mechanism before proceeding, since we're going to be
# deleting client from the relayusers cache.
2016-05-01 23:51:18 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] ' , irc . name , irc . name )
2016-07-02 03:54:35 +02:00
if spawnlocks [ irc . name ] . acquire ( 5 ) :
2016-05-01 08:59:51 +02:00
for netname , user in relayusers [ ( irc . name , numeric ) ] . copy ( ) . items ( ) :
remoteirc = world . networkobjects [ netname ]
2016-05-01 23:21:50 +02:00
try : # Try to quit the client. If this fails because they're missing, bail.
remoteirc . proto . quit ( user , args [ ' text ' ] )
except LookupError :
pass
2016-05-01 08:59:51 +02:00
del relayusers [ ( irc . name , numeric ) ]
2016-07-02 03:54:35 +02:00
spawnlocks [ irc . name ] . release ( )
2016-05-01 08:59:51 +02:00
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_quit , ' QUIT ' )
def handle_squit ( irc , numeric , command , args ) :
2016-06-25 22:47:59 +02:00
"""
Handles SQUITs over relay .
"""
2015-09-15 02:23:56 +02:00
users = args [ ' users ' ]
target = args [ ' target ' ]
2016-06-25 22:47:59 +02:00
2015-09-15 02:23:56 +02:00
# Someone /SQUIT one of our relay subservers. Bad! Rejoin them!
if target in relayservers [ irc . name ] . values ( ) :
sname = args [ ' name ' ]
remotenet = sname . split ( ' . ' , 1 ) [ 0 ]
del relayservers [ irc . name ] [ remotenet ]
2016-06-25 22:47:59 +02:00
2015-09-15 02:23:56 +02:00
for userpair in relayusers :
if userpair [ 0 ] == remotenet and irc . name in relayusers [ userpair ] :
del relayusers [ userpair ] [ irc . name ]
2016-06-25 22:47:59 +02:00
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ remotenet ]
initializeAll ( remoteirc )
2016-06-25 22:47:59 +02:00
2015-09-15 02:23:56 +02:00
else :
# Some other netsplit happened on the network, we'll have to fake
# some *.net *.split quits for that.
for user in users :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_squit: sending handle_quit on %s ' , irc . name , user )
2016-06-25 22:47:59 +02:00
try : # Allow netsplit hiding to be toggled
2016-07-28 02:02:04 +02:00
show_splits = conf . conf [ ' relay ' ] [ ' show_netsplits ' ]
2016-06-25 22:47:59 +02:00
except KeyError :
show_splits = False
text = ' *.net *.split '
if show_splits :
uplink = args [ ' uplink ' ]
try :
text = ' %s %s ' % ( irc . servers [ uplink ] . name , args [ ' name ' ] )
except ( KeyError , AttributeError ) :
log . warning ( " ( %s ) relay.handle_squit: Failed to get server name for %s " ,
irc . name , uplink )
handle_quit ( irc , user , command , { ' text ' : text } )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_squit , ' SQUIT ' )
def handle_nick ( irc , numeric , command , args ) :
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
newnick = normalizeNick ( remoteirc , irc . name , args [ ' newnick ' ] , uid = user )
if remoteirc . users [ user ] . nick != newnick :
2016-01-17 01:51:04 +01:00
remoteirc . proto . nick ( user , newnick )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_nick , ' NICK ' )
def handle_part ( irc , numeric , command , args ) :
channels = args [ ' channels ' ]
text = args [ ' text ' ]
# Don't allow the PyLink client PARTing to be relayed.
if numeric == irc . pseudoclient . uid :
2016-07-24 04:06:35 +02:00
# For clientbot: treat forced parts to the bot as clearchan, and attempt to rejoin only
# if it affected a relay.
2016-07-24 04:03:07 +02:00
if irc . protoname == ' clientbot ' :
2016-07-24 04:06:35 +02:00
for channel in [ c for c in channels if getRelay ( ( irc . name , c ) ) ] :
2016-07-24 04:03:07 +02:00
for user in irc . channels [ channel ] . users :
if ( not irc . isInternalClient ( user ) ) and ( not isRelayClient ( irc , user ) ) :
irc . callHooks ( [ irc . sid , ' CLIENTBOT_SERVICE_KICKED ' , { ' channel ' : channel , ' target ' : user ,
2016-07-29 10:08:14 +02:00
' text ' : ' Clientbot was force parted (Reason: %s ) ' % text or ' None ' ,
' parse_as ' : ' KICK ' } ] )
2016-07-24 04:03:07 +02:00
irc . proto . join ( irc . pseudoclient . uid , channel )
return
2015-09-15 02:23:56 +02:00
return
2016-07-24 04:03:07 +02:00
2015-09-15 02:23:56 +02:00
for channel in channels :
for netname , user in relayusers [ ( irc . name , numeric ) ] . copy ( ) . items ( ) :
remoteirc = world . networkobjects [ netname ]
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-15 02:23:56 +02:00
if remotechan is None :
continue
2016-01-17 01:51:54 +01:00
remoteirc . proto . part ( user , remotechan , text )
2015-09-15 02:23:56 +02:00
if not remoteirc . users [ user ] . channels :
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( user , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , numeric ) ] [ remoteirc . name ]
utils . add_hook ( handle_part , ' PART ' )
2015-09-26 19:10:54 +02:00
def handle_messages ( irc , numeric , command , args ) :
notice = ( command in ( ' NOTICE ' , ' PYLINK_SELF_NOTICE ' ) )
2015-09-15 02:23:56 +02:00
target = args [ ' target ' ]
text = args [ ' text ' ]
2016-01-01 02:28:47 +01:00
if irc . isInternalClient ( numeric ) and irc . isInternalClient ( target ) :
2015-11-27 07:51:19 +01:00
# Drop attempted PMs between internal clients (this shouldn't happen,
# but whatever).
return
2016-02-21 04:29:52 +01:00
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , target ) )
2015-09-15 02:23:56 +02:00
remoteusers = relayusers [ ( irc . name , numeric ) ]
2016-02-21 04:29:52 +01:00
2016-06-25 20:28:51 +02:00
# HACK: Don't break on sending to @#channel or similar. TODO: This should really
# be handled more neatly in core.
2015-09-15 02:23:56 +02:00
try :
prefix , target = target . split ( ' # ' , 1 )
except ValueError :
prefix = ' '
else :
target = ' # ' + target
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
if utils . isChannel ( target ) :
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:29:37 +02:00
real_target = getRemoteChan ( irc , remoteirc , target )
2016-02-21 04:29:52 +01:00
# Don't relay anything back to the source net, or to disconnected networks
2016-05-01 23:38:51 +02:00
# and networks without a relay for this channel.
2016-02-21 04:29:52 +01:00
if irc . name == name or ( not remoteirc . connected . is_set ( ) ) or ( not real_target ) \
or ( not irc . connected . is_set ( ) ) :
2015-09-15 02:23:56 +02:00
continue
2016-02-21 04:29:52 +01:00
2015-09-26 19:10:54 +02:00
user = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
2016-06-25 20:28:51 +02:00
if not user :
# No relay clone exists for the sender; route the message through our
# main client.
if numeric in irc . servers :
displayedname = irc . servers [ numeric ] . name
2016-05-01 23:38:51 +02:00
else :
2016-06-25 20:28:51 +02:00
displayedname = irc . users [ numeric ] . nick
real_text = ' [from %s / %s ] %s ' % ( displayedname , irc . name , text )
try :
user = remoteirc . pseudoclient . uid
except AttributeError :
# Remote main client hasn't spawned yet. Drop the message.
continue
2016-07-02 03:54:07 +02:00
else :
if remoteirc . pseudoclient . uid not in remoteirc . users :
# Remote UID is ghosted, drop message.
continue
2016-06-25 20:28:51 +02:00
else :
real_text = text
real_target = prefix + real_target
log . debug ( ' ( %s ) relay.handle_messages: sending message to %s from %s on behalf of %s ' ,
irc . name , real_target , user , numeric )
2016-07-02 03:54:07 +02:00
try :
if notice :
remoteirc . proto . notice ( user , real_target , real_text )
else :
remoteirc . proto . message ( user , real_target , real_text )
except LookupError :
# Our relay clone disappeared while we were trying to send the message.
# This is normally due to a nick conflict with the IRCd.
log . warning ( " ( %s ) relay: Relay client %s on %s was killed while "
" trying to send a message through it! " , irc . name ,
remoteirc . name , user )
continue
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
else :
2016-02-21 04:29:52 +01:00
# Get the real user that the PM was meant for
origuser = getOrigUser ( irc , target )
if origuser is None : # Not a relay client, return
2015-09-15 02:23:56 +02:00
return
2016-02-21 04:29:52 +01:00
homenet , real_target = origuser
2015-09-15 02:23:56 +02:00
# 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 ( ) :
irc . msg ( numeric , ' Error: You must be in a common channel '
' with %r in order to send messages. ' % \
irc . users [ target ] . nick , notice = True )
return
remoteirc = world . networkobjects [ homenet ]
2016-07-24 06:09:25 +02:00
2016-08-31 23:07:32 +02:00
if remoteirc . protoname == ' clientbot ' and not conf . conf . get ( ' relay ' , { } ) . get ( ' allow_clientbot_pms ' ) :
irc . msg ( numeric , ' Error: Private messages to users connected via Clientbot have '
' been administratively disabled. ' , notice = True )
2016-07-24 06:09:25 +02:00
return
2015-09-15 02:23:56 +02:00
user = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
2016-02-21 04:29:52 +01:00
2016-07-02 03:54:07 +02:00
try :
if notice :
remoteirc . proto . notice ( user , real_target , text )
else :
remoteirc . proto . message ( user , real_target , text )
except LookupError :
# Our relay clone disappeared while we were trying to send the message.
# This is normally due to a nick conflict with the IRCd.
log . warning ( " ( %s ) relay: Relay client %s on %s was killed while "
" trying to send a message through it! " , irc . name ,
remoteirc . name , user )
return
2016-02-21 04:29:52 +01:00
2015-09-26 19:10:54 +02:00
for cmd in ( ' PRIVMSG ' , ' NOTICE ' , ' PYLINK_SELF_NOTICE ' , ' PYLINK_SELF_PRIVMSG ' ) :
utils . add_hook ( handle_messages , cmd )
2015-09-15 02:23:56 +02:00
def handle_kick ( irc , source , command , args ) :
channel = args [ ' channel ' ]
target = args [ ' target ' ]
text = args [ ' text ' ]
kicker = source
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2016-05-15 01:24:26 +02:00
2016-07-23 20:20:52 +02:00
# Special case for clientbot: treat kicks to the PyLink service bot as channel clear.
if irc . protoname == ' clientbot ' and irc . pseudoclient and target == irc . pseudoclient . uid :
for user in irc . channels [ channel ] . users :
if ( not irc . isInternalClient ( user ) ) and ( not isRelayClient ( irc , user ) ) :
2016-07-29 10:08:14 +02:00
reason = " Clientbot kicked by %s (Reason: %s ) " % ( irc . getFriendlyName ( source ) , text )
2016-07-23 20:20:52 +02:00
irc . callHooks ( [ irc . sid , ' CLIENTBOT_SERVICE_KICKED ' , { ' channel ' : channel , ' target ' : user ,
2016-07-29 10:08:14 +02:00
' text ' : reason , ' parse_as ' : ' KICK ' } ] )
2016-07-23 20:20:52 +02:00
return
2016-05-15 01:24:26 +02:00
# Don't relay kicks to protected service bots.
if relay is None or irc . isServiceBot ( target ) :
2015-09-15 02:23:56 +02:00
return
2016-05-15 01:24:26 +02:00
2015-09-15 02:29:37 +02:00
origuser = getOrigUser ( irc , target )
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
continue
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: remotechan for %s on %s is %s ' , irc . name , channel , name , remotechan )
2015-09-15 02:23:56 +02:00
if remotechan is None :
continue
real_kicker = getRemoteUser ( irc , remoteirc , kicker , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: real kicker for %s on %s is %s ' , irc . name , kicker , name , real_kicker )
2015-09-15 02:23:56 +02:00
if not isRelayClient ( irc , target ) :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: target %s is NOT an internal client ' , irc . name , target )
2015-09-15 02:23:56 +02:00
# Both the target and kicker are external clients; i.e.
# they originate from the same network. We won't have
# to filter this; the uplink IRCd will handle it appropriately,
# and we'll just follow.
real_target = getRemoteUser ( irc , remoteirc , target , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: real target for %s is %s ' , irc . name , target , real_target )
2015-09-15 02:23:56 +02:00
else :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: target %s is an internal client, going to look up the real user ' , irc . name , target )
2015-09-15 02:29:37 +02:00
real_target = getOrigUser ( irc , target , targetirc = remoteirc )
2015-09-15 02:23:56 +02:00
if not checkClaim ( irc , channel , kicker ) :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s ' , irc . name , kicker , real_target )
2015-09-15 02:23:56 +02:00
# Home network is not in the channel's claim AND the kicker is not
# opped. We won't propograte the kick then.
# TODO: make the check slightly more advanced: i.e. halfops can't
# kick ops, admins can't kick owners, etc.
modes = getPrefixModes ( remoteirc , irc , remotechan , real_target )
# Join the kicked client back with its respective modes.
2016-01-17 01:53:46 +01:00
irc . proto . sjoin ( irc . sid , channel , [ ( modes , target ) ] )
2015-09-15 02:23:56 +02:00
if kicker in irc . users :
2016-03-27 01:56:11 +01:00
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from %s / %s to relay client %s / %s on %s . ' ,
irc . name , args [ ' text ' ] , irc . users [ source ] . nick , irc . name ,
remoteirc . users [ real_target ] . nick , remoteirc . name , channel )
2015-09-15 02:23:56 +02:00
irc . msg ( kicker , " This channel is claimed; your kick to "
" %s has been blocked because you are not "
" (half)opped. " % channel , notice = True )
else :
2016-03-27 01:56:11 +01:00
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from server %s to relay client %s / %s on %s . ' ,
2015-09-15 02:23:56 +02:00
irc . name , args [ ' text ' ] , irc . servers [ source ] . name ,
remoteirc . users [ real_target ] . nick , remoteirc . name , channel )
return
if not real_target :
continue
# Propogate the kick!
if real_kicker :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s / %s ' , irc . name , real_target , remotechan , real_kicker , kicker , irc . name )
2016-01-17 01:59:01 +01:00
remoteirc . proto . kick ( real_kicker , remotechan , real_target , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
else :
# Kick originated from a server, or the kicker isn't in any
# common channels with the target relay network.
rsid = getRemoteSid ( remoteirc , irc )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s / %s ' , irc . name , real_target , remotechan , rsid , kicker , irc . name )
2016-07-23 21:25:52 +02:00
if irc . protoname == ' clientbot ' :
# Special case for clientbot: no kick prefixes are needed.
text = args [ ' text ' ]
else :
try :
if kicker in irc . servers :
kname = irc . servers [ kicker ] . name
else :
kname = irc . users . get ( kicker ) . nick
text = " ( %s / %s ) %s " % ( kname , irc . name , args [ ' text ' ] )
except AttributeError :
text = " (<unknown kicker>@ %s ) %s " % ( irc . name , args [ ' text ' ] )
2016-01-17 01:59:01 +01:00
remoteirc . proto . kick ( rsid , remotechan , real_target , text )
2015-09-15 02:23:56 +02:00
# If the target isn't on any channels, quit them.
2015-10-18 19:25:59 +02:00
if remoteirc != irc and ( not remoteirc . users [ real_target ] . channels ) and not origuser :
del relayusers [ ( irc . name , target ) ] [ remoteirc . name ]
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( real_target , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
if origuser and not irc . users [ target ] . channels :
del relayusers [ origuser ] [ irc . name ]
2016-01-17 01:51:54 +01:00
irc . proto . quit ( target , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_kick , ' KICK ' )
def handle_chgclient ( irc , source , command , args ) :
target = args [ ' target ' ]
if args . get ( ' newhost ' ) :
field = ' HOST '
text = args [ ' newhost ' ]
elif args . get ( ' newident ' ) :
field = ' IDENT '
text = args [ ' newident ' ]
elif args . get ( ' newgecos ' ) :
field = ' GECOS '
text = args [ ' newgecos ' ]
if field :
for netname , user in relayusers [ ( irc . name , target ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
try :
2015-11-22 08:37:19 +01:00
if field == ' HOST ' :
2016-08-22 03:06:53 +02:00
newtext = normalizeHost ( remoteirc , text )
else : # Don't overwrite the original text variable on every iteration.
newtext = text
remoteirc . proto . updateClient ( user , field , newtext )
2015-09-15 02:23:56 +02:00
except NotImplementedError : # IRCd doesn't support changing the field we want
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_chgclient: Ignoring changing field %r of %s on %s (for %s / %s ); '
2015-09-15 02:23:56 +02:00
' remote IRCd doesn \' t support it ' , irc . name , field ,
2015-12-31 00:53:05 +01:00
user , netname , target , irc . name )
2015-09-15 02:23:56 +02:00
continue
for c in ( ' CHGHOST ' , ' CHGNAME ' , ' CHGIDENT ' ) :
utils . add_hook ( handle_chgclient , c )
2015-07-20 07:43:26 +02:00
2015-07-15 04:39:49 +02:00
def handle_mode ( irc , numeric , command , args ) :
target = args [ ' target ' ]
modes = args [ ' modes ' ]
2016-07-24 20:11:54 +02:00
2016-07-29 09:54:29 +02:00
if irc . protoname == ' clientbot ' and utils . isChannel ( target ) :
# We don't sync cmodes with clientbot networks yet.
2016-07-24 20:11:54 +02:00
return
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-07-26 07:56:34 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
2015-07-15 04:39:49 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-07-20 07:43:26 +02:00
if utils . isChannel ( target ) :
2016-06-23 04:49:49 +02:00
# Use the old state of the channel to check for CLAIM access.
2016-09-03 02:52:19 +02:00
oldchan = args . get ( ' channeldata ' )
2016-02-28 03:36:20 +01:00
2015-09-13 22:48:14 +02:00
if checkClaim ( irc , target , numeric , chanobj = oldchan ) :
2016-06-23 04:49:49 +02:00
remotechan = getRemoteChan ( irc , remoteirc , target )
supported_modes = getSupportedCmodes ( irc , remoteirc , target , modes )
if supported_modes :
# Check if the sender is a user with a relay client; otherwise relay the mode
# from the corresponding server.
u = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
if u :
remoteirc . proto . mode ( u , remotechan , supported_modes )
else :
rsid = getRemoteSid ( remoteirc , irc )
remoteirc . proto . mode ( rsid , remotechan , supported_modes )
2015-09-13 08:36:52 +02:00
else : # Mode change blocked by CLAIM.
2016-05-01 01:33:46 +02:00
reversed_modes = irc . reverseModes ( target , modes , oldobj = oldchan )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_mode: Reversing mode changes of %r with %r (CLAIM). ' ,
2015-09-13 08:36:52 +02:00
irc . name , modes , reversed_modes )
2016-06-22 05:29:36 +02:00
if reversed_modes :
irc . proto . mode ( irc . sid , target , reversed_modes )
2015-09-13 08:36:52 +02:00
break
2016-02-28 03:36:20 +01:00
2015-07-20 07:43:26 +02:00
else :
2015-08-31 23:23:42 +02:00
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc . umodes . get ( ' hideoper ' )
2015-07-20 07:43:26 +02:00
modes = getSupportedUmodes ( irc , remoteirc , modes )
2016-02-28 03:36:20 +01:00
2015-08-31 23:23:42 +02:00
if hideoper_mode :
if ( ' +o ' , None ) in modes :
modes . append ( ( ' + %s ' % hideoper_mode , None ) )
elif ( ' -o ' , None ) in modes :
modes . append ( ( ' - %s ' % hideoper_mode , None ) )
2016-02-28 03:36:20 +01:00
2015-07-20 08:49:50 +02:00
remoteuser = getRemoteUser ( irc , remoteirc , target , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
2015-09-02 07:13:29 +02:00
if remoteuser and modes :
2016-01-17 02:08:17 +01:00
remoteirc . proto . mode ( remoteuser , remoteuser , modes )
2015-07-20 07:43:26 +02:00
2015-07-15 04:39:49 +02:00
utils . add_hook ( handle_mode , ' MODE ' )
2015-07-15 08:24:21 +02:00
def handle_topic ( irc , numeric , command , args ) :
channel = args [ ' channel ' ]
2015-09-14 02:58:39 +02:00
oldtopic = args . get ( ' oldtopic ' )
2015-12-19 06:53:35 +01:00
topic = args [ ' text ' ]
2015-09-13 23:23:27 +02:00
if checkClaim ( irc , channel , numeric ) :
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-13 23:23:27 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
continue
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-13 23:23:27 +02:00
# Don't send if the remote topic is the same as ours.
if remotechan is None or topic == remoteirc . channels [ remotechan ] . topic :
continue
# This might originate from a server too.
remoteuser = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
if remoteuser :
2016-01-17 02:09:52 +01:00
remoteirc . proto . topic ( remoteuser , remotechan , topic )
2015-09-13 23:23:27 +02:00
else :
rsid = getRemoteSid ( remoteirc , irc )
2016-01-17 02:09:52 +01:00
remoteirc . proto . topicBurst ( rsid , remotechan , topic )
2015-09-14 02:58:39 +02:00
elif oldtopic : # Topic change blocked by claim.
2016-05-23 04:12:11 +02:00
irc . proto . topicBurst ( irc . sid , channel , oldtopic )
2015-07-15 08:24:21 +02:00
utils . add_hook ( handle_topic , ' TOPIC ' )
2015-07-15 08:25:40 +02:00
def handle_kill ( irc , numeric , command , args ) :
target = args [ ' target ' ]
userdata = args [ ' userdata ' ]
2016-03-08 03:10:36 +01:00
# Try to find the original client of the target being killed
if userdata and hasattr ( userdata , ' remote ' ) :
realuser = userdata . remote
else :
realuser = getOrigUser ( irc , target )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kill: realuser is %r ' , irc . name , realuser )
2016-03-08 03:10:36 +01:00
2015-07-25 03:26:31 +02:00
# Target user was remote:
if realuser and realuser [ 0 ] != irc . name :
# We don't allow killing over the relay, so we must respawn the affected
# client and rejoin it to its channels.
del relayusers [ realuser ] [ irc . name ]
2016-03-26 00:39:06 +01:00
remoteirc = world . networkobjects [ realuser [ 0 ] ]
for remotechan in remoteirc . users [ realuser [ 1 ] ] . channels :
localchan = getRemoteChan ( remoteirc , irc , remotechan )
if localchan :
modes = getPrefixModes ( remoteirc , irc , remotechan , realuser [ 1 ] )
log . debug ( ' ( %s ) relay.handle_kill: userpair: %s , %s ' , irc . name , modes , realuser )
2016-07-11 08:48:10 +02:00
client = getRemoteUser ( remoteirc , irc , realuser [ 1 ] , times_tagged = 1 )
2016-03-26 00:39:06 +01:00
irc . proto . sjoin ( getRemoteSid ( irc , remoteirc ) , localchan , [ ( modes , client ) ] )
if userdata and numeric in irc . users :
log . info ( ' ( %s ) relay.handle_kill: Blocked KILL (reason %r ) from %s to relay client %s / %s . ' ,
irc . name , args [ ' text ' ] , irc . users [ numeric ] . nick ,
remoteirc . users [ realuser [ 1 ] ] . nick , realuser [ 0 ] )
irc . msg ( numeric , " Your kill to %s has been blocked "
" because PyLink does not allow killing "
" users over the relay at this time. " % \
userdata . nick , notice = True )
2015-08-23 06:43:25 +02:00
else :
2016-03-26 00:39:06 +01:00
log . info ( ' ( %s ) relay.handle_kill: Blocked KILL (reason %r ) from server %s to relay client %s / %s . ' ,
irc . name , args [ ' text ' ] , irc . servers [ numeric ] . name ,
remoteirc . users [ realuser [ 1 ] ] . nick , realuser [ 0 ] )
2015-09-13 02:41:49 +02:00
2015-07-25 03:26:31 +02:00
# Target user was local.
else :
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages
# for locally killed clients, while others (inspircd) do!
# If we receive a user object in 'userdata' instead of None, it means
# that the KILL hasn't been handled by a preceding QUIT message.
if userdata :
handle_quit ( irc , target , ' KILL ' , { ' text ' : args [ ' text ' ] } )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_kill , ' KILL ' )
def handle_away ( irc , numeric , command , args ) :
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
2016-01-17 01:40:36 +01:00
remoteirc . proto . away ( user , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_away , ' AWAY ' )
def handle_invite ( irc , source , command , args ) :
target = args [ ' target ' ]
channel = args [ ' channel ' ]
if isRelayClient ( irc , target ) :
2015-09-15 02:29:37 +02:00
remotenet , remoteuser = getOrigUser ( irc , target )
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ remotenet ]
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-15 02:23:56 +02:00
remotesource = getRemoteUser ( irc , remoteirc , source , spawnIfMissing = False )
if remotesource is None :
irc . msg ( source , ' Error: You must be in a common channel '
' with %s to invite them to channels. ' % \
irc . users [ target ] . nick ,
notice = True )
elif remotechan is None :
irc . msg ( source , ' Error: You cannot invite someone to a '
' channel not on their network! ' ,
notice = True )
else :
2016-01-17 01:38:27 +01:00
remoteirc . proto . invite ( remotesource , remoteuser ,
2015-09-15 02:23:56 +02:00
remotechan )
utils . add_hook ( handle_invite , ' INVITE ' )
2015-09-29 03:15:56 +02:00
def handle_endburst ( irc , numeric , command , args ) :
if numeric == irc . uplink :
initializeAll ( irc )
utils . add_hook ( handle_endburst , " ENDBURST " )
2016-07-08 06:43:25 +02:00
def handle_services_login ( irc , numeric , command , args ) :
"""
Relays services account changes as a hook , for integration with plugins like Automode .
"""
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
remoteirc . callHooks ( [ user , ' PYLINK_RELAY_SERVICES_LOGIN ' , args ] )
utils . add_hook ( handle_services_login , ' CLIENT_SERVICES_LOGIN ' )
2015-09-15 02:23:56 +02:00
def handle_disconnect ( irc , numeric , command , args ) :
2015-10-11 00:34:57 +02:00
""" Handles IRC network disconnections (internal hook). """
# Quit all of our users' representations on other nets, and remove
# them from our relay clients index.
2016-05-01 23:51:18 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] ' , irc . name , irc . name )
2016-07-02 03:54:35 +02:00
if spawnlocks [ irc . name ] . acquire ( 5 ) :
2016-03-31 03:33:44 +02:00
for k , v in relayusers . copy ( ) . items ( ) :
if irc . name in v :
del relayusers [ k ] [ irc . name ]
if k [ 0 ] == irc . name :
2016-06-25 22:13:59 +02:00
del relayusers [ k ]
2016-07-02 03:54:35 +02:00
spawnlocks [ irc . name ] . release ( )
2016-05-01 08:59:51 +02:00
2015-10-11 00:34:57 +02:00
# SQUIT all relay pseudoservers spawned for us, and remove them
# from our relay subservers index.
2016-05-01 23:51:18 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks_servers[ %s ] ' , irc . name , irc . name )
2016-07-02 03:54:35 +02:00
if spawnlocks_servers [ irc . name ] . acquire ( 5 ) :
2016-03-31 03:33:44 +02:00
for name , ircobj in world . networkobjects . copy ( ) . items ( ) :
2016-05-01 08:59:51 +02:00
if name != irc . name :
2016-03-31 03:33:44 +02:00
try :
rsid = relayservers [ name ] [ irc . name ]
except KeyError :
continue
else :
ircobj . proto . squit ( ircobj . sid , rsid , text = ' Relay network lost connection. ' )
2016-05-01 08:59:51 +02:00
if irc . name in relayservers [ name ] :
2016-03-31 03:33:44 +02:00
del relayservers [ name ] [ irc . name ]
2016-03-25 23:59:37 +01:00
2016-07-02 07:20:10 +02:00
try :
del relayservers [ irc . name ]
except KeyError : # Already removed; ignore.
pass
2016-07-02 03:54:35 +02:00
spawnlocks_servers [ irc . name ] . release ( )
2016-03-25 23:59:37 +01:00
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_disconnect , " PYLINK_DISCONNECT " )
2015-07-13 08:28:54 +02:00
2016-07-11 08:48:10 +02:00
def nick_collide ( irc , target ) :
"""
Handles nick collisions on relay clients and attempts to fix nicks .
"""
remotenet , remoteuser = getOrigUser ( irc , target )
remoteirc = world . networkobjects [ remotenet ]
nick = remoteirc . users [ remoteuser ] . nick
2016-07-12 06:54:10 +02:00
# Force a tagged nick by setting times_tagged to 1.
newnick = normalizeNick ( irc , remotenet , nick , times_tagged = 1 )
2016-07-11 08:48:10 +02:00
log . info ( ' ( %s ) relay.nick_collide: Fixing nick of relay client %r ( %s ) to %s ' ,
irc . name , target , nick , newnick )
irc . proto . nick ( target , newnick )
2015-09-15 02:23:56 +02:00
def handle_save ( irc , numeric , command , args ) :
target = args [ ' target ' ]
2016-07-11 08:48:10 +02:00
if isRelayClient ( irc , target ) :
2015-09-15 02:23:56 +02:00
# Nick collision!
# It's one of our relay clients; try to fix our nick to the next
# available normalized nick.
2016-07-11 08:48:10 +02:00
nick_collide ( irc , target )
2015-09-15 02:23:56 +02:00
else :
# Somebody else on the network (not a PyLink client) had a nick collision;
# relay this as a nick change appropriately.
handle_nick ( irc , target , ' SAVE ' , { ' oldnick ' : None , ' newnick ' : target } )
2015-07-14 08:29:20 +02:00
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_save , " SAVE " )
2016-07-12 08:23:26 +02:00
def handle_svsnick ( irc , numeric , command , args ) :
"""
Handles forced nick change attempts to relay clients , tagging their nick .
"""
target = args [ ' target ' ]
if isRelayClient ( irc , target ) :
nick_collide ( irc , target )
utils . add_hook ( handle_svsnick , " SVSNICK " )
2015-09-15 02:23:56 +02:00
### PUBLIC COMMANDS
2015-07-13 04:03:18 +02:00
2015-07-11 05:26:46 +02:00
def create ( irc , source , args ) :
""" <channel>
2016-07-01 03:05:27 +02:00
Opens up the given channel over PyLink Relay . """
2015-07-11 05:26:46 +02:00
try :
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 0 ] )
2015-07-11 05:26:46 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1: channel. " )
2015-07-12 22:09:35 +02:00
return
2015-07-11 05:26:46 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-08-04 19:55:00 +02:00
if irc . protoname == ' clientbot ' :
irc . reply ( ' Error: Clientbot networks cannot be used to host a relay. ' )
return
2015-07-13 02:59:09 +02:00
if source not in irc . channels [ channel ] . users :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: You must be in %r to complete this operation. ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-08-04 19:55:00 +02:00
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-11-29 06:09:16 +01:00
# Check to see whether the channel requested is already part of a different
# relay.
2015-09-18 04:25:51 +02:00
localentry = getRelay ( ( irc . name , channel ) )
2015-09-18 04:24:38 +02:00
if localentry :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Channel %r is already part of a relay. ' % channel )
2015-09-18 04:24:38 +02:00
return
2015-11-29 06:09:16 +01:00
2016-05-01 01:44:37 +02:00
creator = irc . getHostmask ( source )
2015-11-29 06:09:16 +01:00
# Create the relay database entry with the (network name, channel name)
# pair - this is just a dict with various keys.
db [ ( irc . name , channel ) ] = { ' claim ' : [ irc . name ] , ' links ' : set ( ) ,
2016-01-10 05:30:54 +01:00
' blocked_nets ' : set ( ) , ' creator ' : creator ,
' ts ' : time . time ( ) }
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s created by %s . ' , irc . name , channel , creator )
2015-07-13 08:28:54 +02:00
initializeChannel ( irc , channel )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2016-07-01 03:05:27 +02:00
create = utils . add_cmd ( create , featured = True )
2015-07-11 05:26:46 +02:00
def destroy ( irc , source , args ) :
2016-03-05 18:31:59 +01:00
""" [<home network>] <channel>
2015-07-11 05:26:46 +02:00
2016-07-01 03:05:27 +02:00
Removes the given channel from the PyLink Relay , delinking all networks linked to it . If the home network is given and you are logged in as admin , this can also remove relay channels from other networks . """
2016-03-05 18:31:59 +01:00
try : # Two args were given: first one is network name, second is channel.
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 1 ] )
2016-03-05 18:31:59 +01:00
network = args [ 0 ]
2015-07-11 05:26:46 +02:00
except IndexError :
2016-03-05 18:31:59 +01:00
try : # One argument was given; assume it's just the channel.
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 0 ] )
2016-03-05 18:31:59 +01:00
network = irc . name
except IndexError :
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, network (optional). " )
return
2015-07-11 05:26:46 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-03-05 18:31:59 +01:00
if network == irc . name :
# If we're destroying a channel on the current network, only oper is needed.
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2016-03-05 18:31:59 +01:00
else :
# Otherwise, we'll need to be logged in as admin.
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source , allowOper = False )
2016-03-05 18:31:59 +01:00
entry = ( network , channel )
2015-07-14 08:29:20 +02:00
if entry in db :
2016-03-05 18:31:59 +01:00
# Iterate over all the channel links and deinitialize them.
2015-07-14 08:29:20 +02:00
for link in db [ entry ] [ ' links ' ] :
2015-08-29 18:39:33 +02:00
removeChannel ( world . networkobjects . get ( link [ 0 ] ) , link [ 1 ] )
2016-03-05 18:31:59 +01:00
removeChannel ( world . networkobjects . get ( network ) , channel )
2015-07-14 08:29:20 +02:00
del db [ entry ]
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s destroyed by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
channel , irc . getHostmask ( source ) )
2016-03-05 18:31:59 +01:00
irc . reply ( ' Done. ' )
2015-07-11 05:26:46 +02:00
else :
2016-06-19 21:32:27 +02:00
irc . reply ( " Error: No such channel %r exists. If you ' re trying to delink a channel from "
" another network, use the DESTROY command. " % channel )
2015-07-13 02:59:09 +02:00
return
2016-07-01 03:05:27 +02:00
destroy = utils . add_cmd ( destroy , featured = True )
2015-07-11 05:26:46 +02:00
2015-07-13 02:59:09 +02:00
def link ( irc , source , args ) :
""" <remotenet> <channel> <local channel>
2016-07-01 03:05:27 +02:00
Links the specified channel on \x02remotenet \x02 over PyLink Relay as \x02local channel \x02 .
If \x02local channel \x02 is not specified , it defaults to the same name as \x02channel \x02 . """
2015-07-13 02:59:09 +02:00
try :
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 1 ] )
2016-04-02 21:15:53 +02:00
remotenet = args [ 0 ]
2015-07-13 02:59:09 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional). " )
2015-07-13 02:59:09 +02:00
return
2016-06-19 21:13:56 +02:00
2015-07-13 02:59:09 +02:00
try :
2016-05-01 01:57:38 +02:00
localchan = irc . toLower ( args [ 2 ] )
2015-07-13 02:59:09 +02:00
except IndexError :
localchan = channel
2016-06-19 21:13:56 +02:00
2015-07-13 02:59:09 +02:00
for c in ( channel , localchan ) :
if not utils . isChannel ( c ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % c )
2015-07-13 02:59:09 +02:00
return
2016-06-19 21:13:56 +02:00
if remotenet == irc . name :
irc . reply ( ' Error: Cannot link two channels on the same network. ' )
return
2015-07-13 02:59:09 +02:00
if source not in irc . channels [ localchan ] . users :
2016-08-04 19:47:06 +02:00
# Caller is not in the requested channel.
log . debug ( ' ( %s ) Source not in channel %s ; protoname= %s ' , irc . name , localchan , irc . protoname )
if irc . protoname == ' clientbot ' :
# Special case for Clientbot: join the requested channel first, then
# require that the caller be opped.
if localchan not in irc . pseudoclient . channels :
irc . proto . join ( irc . pseudoclient . uid , localchan )
2016-08-11 21:51:08 +02:00
irc . reply ( ' Joining %r now to check for op status; please run this command again after I join. ' % localchan )
2016-08-04 19:47:06 +02:00
return
elif not irc . channels [ localchan ] . isOpPlus ( source ) :
irc . reply ( ' Error: You must be opped in %r to complete this operation. ' % localchan )
return
else :
irc . reply ( ' Error: You must be in %r to complete this operation. ' % localchan )
return
2016-06-19 21:13:56 +02:00
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2016-06-19 21:13:56 +02:00
2015-08-29 18:39:33 +02:00
if remotenet not in world . networkobjects :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No network named %r exists. ' % remotenet )
2015-07-13 02:59:09 +02:00
return
2015-09-15 02:29:37 +02:00
localentry = getRelay ( ( irc . name , localchan ) )
2016-06-19 21:13:56 +02:00
2015-07-14 07:54:51 +02:00
if localentry :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Channel %r is already part of a relay. ' % localchan )
2015-07-13 02:59:09 +02:00
return
2016-06-19 21:13:56 +02:00
2015-07-13 02:59:09 +02:00
try :
entry = db [ ( remotenet , channel ) ]
except KeyError :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-07-13 02:59:09 +02:00
return
else :
2015-08-26 05:18:14 +02:00
if irc . name in entry [ ' blocked_nets ' ] :
2016-06-09 01:00:25 +02:00
irc . reply ( ' Error: Access denied (target channel is not open to links). ' )
2015-08-26 05:18:14 +02:00
return
2015-07-14 07:54:51 +02:00
for link in entry [ ' links ' ] :
if link [ 0 ] == irc . name :
2015-11-29 06:18:30 +01:00
irc . reply ( " Error: Remote channel ' %s %s ' is already linked here "
" as %r . " % ( remotenet , channel , link [ 1 ] ) )
2015-07-14 07:54:51 +02:00
return
2016-08-04 22:04:32 +02:00
our_ts = irc . channels [ localchan ] . ts
their_ts = world . networkobjects [ remotenet ] . channels [ channel ] . ts
if our_ts < their_ts :
log . debug ( ' ( %s ) relay: Blocking link request %s %s -> %s %s due to bad TS ( %s < %s ) ' , irc . name ,
irc . name , localchan , remotenet , channel , our_ts , their_ts )
irc . reply ( " Error: the channel creation date (TS) on %s is lower than the target "
" channel ' s; refusing to link. You should clear the local channel %s first "
" before linking, or use a different local channel. " % ( localchan , localchan ) )
return
2015-07-13 02:59:09 +02:00
entry [ ' links ' ] . add ( ( irc . name , localchan ) )
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s linked to %s %s by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
localchan , remotenet , channel , irc . getHostmask ( source ) )
2015-07-13 08:28:54 +02:00
initializeChannel ( irc , localchan )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2016-07-01 03:05:27 +02:00
link = utils . add_cmd ( link , featured = True )
2015-07-12 22:09:35 +02:00
2015-07-13 02:59:09 +02:00
def delink ( irc , source , args ) :
""" <local channel> [<network>]
2016-07-01 03:05:27 +02:00
Delinks the given channel from PyLink Relay . \x02network \x02 must and can only be specified if you are on the host network for the channel given , and allows you to pick which network to delink .
To remove a relay channel entirely , use the ' destroy ' command instead . """
2015-07-13 02:59:09 +02:00
try :
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 0 ] )
2015-07-13 02:59:09 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, remote netname (optional). " )
2015-07-13 02:59:09 +02:00
return
try :
2016-04-02 21:15:53 +02:00
remotenet = args [ 1 ]
2015-07-13 02:59:09 +02:00
except IndexError :
remotenet = None
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-07-13 02:59:09 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-13 02:59:09 +02:00
return
2015-09-15 02:29:37 +02:00
entry = getRelay ( ( irc . name , channel ) )
2015-07-14 07:54:51 +02:00
if entry :
if entry [ 0 ] == irc . name : # We own this channel.
2015-07-26 01:56:20 +02:00
if not remotenet :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: You must select a network to "
2015-08-12 16:03:49 +02:00
" delink, or use the ' destroy ' command to remove "
" this relay entirely (it was created on the current "
" network). " )
2015-07-14 07:54:51 +02:00
return
else :
2015-07-15 20:47:06 +02:00
for link in db [ entry ] [ ' links ' ] . copy ( ) :
if link [ 0 ] == remotenet :
2015-08-29 18:39:33 +02:00
removeChannel ( world . networkobjects . get ( remotenet ) , link [ 1 ] )
2015-07-15 20:47:06 +02:00
db [ entry ] [ ' links ' ] . remove ( link )
2015-07-13 02:59:09 +02:00
else :
2015-07-14 07:54:51 +02:00
removeChannel ( irc , channel )
2015-07-14 08:29:20 +02:00
db [ entry ] [ ' links ' ] . remove ( ( irc . name , channel ) )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s delinked from %s %s by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
channel , entry [ 0 ] , entry [ 1 ] , irc . getHostmask ( source ) )
2015-07-13 02:59:09 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r . ' % channel )
2016-07-01 03:05:27 +02:00
delink = utils . add_cmd ( delink , featured = True )
2015-07-13 02:59:09 +02:00
2015-07-18 07:00:25 +02:00
def linked ( irc , source , args ) :
2016-05-22 07:55:06 +02:00
""" [<network>]
2015-07-18 07:35:34 +02:00
2016-07-01 03:05:27 +02:00
Returns a list of channels shared across PyLink Relay . If \x02network \x02 is given , filters output to channels linked to the given network . """
2016-02-22 05:04:53 +01:00
# Only show remote networks that are marked as connected.
2016-03-27 03:19:08 +02:00
remote_networks = [ netname for netname , ircobj in world . networkobjects . copy ( ) . items ( )
2016-02-22 05:04:53 +01:00
if ircobj . connected . is_set ( ) ]
# But remove the current network from the list, so that it isn't shown twice.
remote_networks . remove ( irc . name )
remote_networks . sort ( )
s = ' Connected networks: \x02 %s \x02 %s ' % ( irc . name , ' ' . join ( remote_networks ) )
2016-07-01 03:43:35 +02:00
# Always reply in private to prevent floods.
irc . reply ( s , private = True )
2015-11-29 06:09:16 +01:00
2016-05-22 07:55:06 +02:00
net = ' '
try :
net = args [ 0 ]
except :
pass
else :
2016-07-01 03:43:35 +02:00
irc . reply ( " Showing channels linked to %s : " % net , private = True )
2016-05-22 07:55:06 +02:00
2015-11-29 06:09:16 +01:00
# Sort the list of shared channels when displaying
for k , v in sorted ( db . items ( ) ) :
2016-02-22 05:04:53 +01:00
2016-05-22 07:55:06 +02:00
# Skip if we're filtering by network and the network given isn't relayed
# to the channel.
if net and not ( net == k [ 0 ] or net in [ link [ 0 ] for link in v [ ' links ' ] ] ) :
continue
2015-11-29 06:09:16 +01:00
# Bold each network/channel name pair
2015-07-18 07:00:25 +02:00
s = ' \x02 %s %s \x02 ' % k
2015-09-19 07:05:51 +02:00
remoteirc = world . networkobjects . get ( k [ 0 ] )
2015-11-29 06:09:16 +01:00
channel = k [ 1 ] # Get the channel name from the network/channel pair
2016-02-22 05:04:53 +01:00
2015-09-19 07:05:51 +02:00
if remoteirc and channel in remoteirc . channels :
c = remoteirc . channels [ channel ]
if ( ' s ' , None ) in c . modes or ( ' p ' , None ) in c . modes :
2016-07-17 06:07:08 +02:00
# Only show secret channels to opers or those in the channel, and tag them as
2015-11-29 06:09:16 +01:00
# [secret].
2016-07-17 06:07:08 +02:00
localchan = getRemoteChan ( remoteirc , irc , channel )
if irc . isOper ( source ) or ( localchan and source in irc . channels [ localchan ] . users ) :
2015-09-19 07:05:51 +02:00
s + = ' \x02 [secret] \x02 '
else :
continue
2015-11-29 06:09:16 +01:00
2016-07-03 21:40:23 +02:00
if v [ ' links ' ] :
# Sort, join up and output all the linked channel names. Silently drop
# entries for disconnected networks.
s + = ' ' . join ( [ ' ' . join ( link ) for link in sorted ( v [ ' links ' ] ) if link [ 0 ] in world . networkobjects
and world . networkobjects [ link [ 0 ] ] . connected . is_set ( ) ] )
2016-02-22 05:04:53 +01:00
2015-11-29 06:09:16 +01:00
else : # Unless it's empty; then, well... just say no relays yet.
2015-07-18 07:00:25 +02:00
s + = ' (no relays yet) '
2015-11-29 06:09:16 +01:00
2016-07-01 03:43:35 +02:00
irc . reply ( s , private = True )
2015-08-12 13:18:20 +02:00
2016-05-01 01:54:11 +02:00
if irc . isOper ( source ) :
2016-01-10 05:30:54 +01:00
s = ' '
2015-11-29 06:09:16 +01:00
# If the caller is an oper, we can show the hostmasks of people
# that created all the available channels (Janus does this too!!)
2015-12-27 00:24:06 +01:00
creator = v . get ( ' creator ' )
if creator :
# But only if the value actually exists (old DBs will have it
# missing).
2016-01-10 05:30:54 +01:00
s + = ' by \x02 %s \x02 ' % creator
# Ditto for creation date
ts = v . get ( ' ts ' )
if ts :
s + = ' on %s ' % time . ctime ( ts )
2016-02-22 05:04:53 +01:00
if s : # Indent to make the list look nicer
2016-07-01 03:43:35 +02:00
irc . reply ( ' Channel created %s . ' % s , private = True )
2016-07-01 03:05:27 +02:00
linked = utils . add_cmd ( linked , featured = True )
2015-11-29 06:09:16 +01:00
2015-08-26 05:18:14 +02:00
@utils.add_cmd
def linkacl ( irc , source , args ) :
""" ALLOW|DENY|LIST <channel> <remotenet>
2016-07-01 03:05:27 +02:00
Allows blocking / unblocking certain networks from linking to a relayed channel , based on a blacklist .
2015-08-26 05:18:14 +02:00
LINKACL LIST returns a list of blocked networks for a channel , while the ALLOW and DENY subcommands allow manipulating this blacklist . """
2015-08-26 05:55:39 +02:00
missingargs = " Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY). "
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-08-26 05:18:14 +02:00
try :
cmd = args [ 0 ] . lower ( )
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 1 ] )
2015-08-26 05:18:14 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( missingargs )
2015-08-26 05:18:14 +02:00
return
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-08-26 05:18:14 +02:00
return
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-08-26 05:18:14 +02:00
if not relay :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-08-26 05:18:14 +02:00
return
if cmd == ' list ' :
s = ' Blocked networks for \x02 %s \x02 : \x02 %s \x02 ' % ( channel , ' , ' . join ( db [ relay ] [ ' blocked_nets ' ] ) or ' (empty) ' )
2015-10-24 03:29:10 +02:00
irc . reply ( s )
2015-08-26 05:18:14 +02:00
return
try :
remotenet = args [ 2 ]
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( missingargs )
2015-08-26 05:18:14 +02:00
return
if cmd == ' deny ' :
db [ relay ] [ ' blocked_nets ' ] . add ( remotenet )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-08-26 05:18:14 +02:00
elif cmd == ' allow ' :
try :
db [ relay ] [ ' blocked_nets ' ] . remove ( remotenet )
except KeyError :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Network %r is not on the blacklist for %r . ' % ( remotenet , channel ) )
2015-08-26 05:18:14 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-08-26 05:18:14 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Unknown subcommand %r : valid ones are ALLOW, DENY, and LIST. ' % cmd )
2015-08-30 04:29:05 +02:00
@utils.add_cmd
def showuser ( irc , source , args ) :
""" <user>
2016-07-01 03:05:27 +02:00
Shows relay data about the given user . This supplements the ' showuser ' command in the ' commands ' plugin , which provides more general information . """
2015-08-30 04:29:05 +02:00
try :
target = args [ 0 ]
except IndexError :
# No errors here; showuser from the commands plugin already does this
# for us.
return
2016-01-01 02:28:47 +01:00
u = irc . nickToUid ( target )
2015-08-30 04:49:37 +02:00
if u :
2016-07-01 03:43:35 +02:00
irc . reply ( " Showing relay information on user \x02 %s \x02 : " % irc . users [ u ] . nick , private = True )
2015-08-30 04:29:05 +02:00
try :
2015-09-15 02:29:37 +02:00
userpair = getOrigUser ( irc , u ) or ( irc . name , u )
2015-08-30 04:29:05 +02:00
remoteusers = relayusers [ userpair ] . items ( )
except KeyError :
pass
else :
nicks = [ ]
if remoteusers :
2015-09-13 01:03:59 +02:00
nicks . append ( ' %s : \x02 %s \x02 ' % ( userpair [ 0 ] ,
2015-08-30 04:49:37 +02:00
world . networkobjects [ userpair [ 0 ] ] . users [ userpair [ 1 ] ] . nick ) )
2015-08-30 04:29:05 +02:00
for r in remoteusers :
remotenet , remoteuser = r
remoteirc = world . networkobjects [ remotenet ]
2015-09-13 01:03:59 +02:00
nicks . append ( ' %s : \x02 %s \x02 ' % ( remotenet , remoteirc . users [ remoteuser ] . nick ) )
2016-07-01 03:43:35 +02:00
irc . reply ( " \x02 Relay nicks \x02 : %s " % ' , ' . join ( nicks ) , private = True )
2015-08-30 04:29:05 +02:00
relaychannels = [ ]
for ch in irc . users [ u ] . channels :
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , ch ) )
2015-08-30 04:29:05 +02:00
if relay :
relaychannels . append ( ' ' . join ( relay ) )
2016-05-01 01:54:11 +02:00
if relaychannels and ( irc . isOper ( source ) or u == source ) :
2016-07-01 03:43:35 +02:00
irc . reply ( " \x02 Relay channels \x02 : %s " % ' ' . join ( relaychannels ) , private = True )
2015-09-15 02:23:56 +02:00
@utils.add_cmd
def save ( irc , source , args ) :
""" takes no arguments.
Saves the relay database to disk . """
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-09-18 04:22:34 +02:00
exportDB ( )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-09-17 05:59:32 +02:00
@utils.add_cmd
def claim ( irc , source , args ) :
""" <channel> [<comma separated list of networks>]
Sets the CLAIM for a channel to a case - sensitive list of networks . If no list of networks is given , shows which networks have claim over the channel . A single hyphen ( - ) can also be given as a list of networks to remove claim from the channel entirely .
CLAIM is a way of enforcing network ownership for a channel , similarly to Janus . Unless the list is empty , only networks on the CLAIM list for a channel ( plus the creating network ) are allowed to override kicks , mode changes , and topic changes in it - attempts from other networks ' opers to do this are simply blocked or reverted. " " "
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-09-17 05:59:32 +02:00
try :
2016-05-01 01:57:38 +02:00
channel = irc . toLower ( args [ 0 ] )
2015-09-17 05:59:32 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, list of networks (optional). " )
2015-09-17 05:59:32 +02:00
return
# We override getRelay() here to limit the search to the current network.
relay = ( irc . name , channel )
if relay not in db :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-09-17 05:59:32 +02:00
return
claimed = db [ relay ] [ " claim " ]
try :
nets = args [ 1 ] . strip ( )
except IndexError : # No networks given.
2015-10-24 03:29:10 +02:00
irc . reply ( ' Channel \x02 %s \x02 is claimed by: %s ' %
2015-09-26 18:39:45 +02:00
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )
2015-09-17 05:59:32 +02:00
else :
if nets == ' - ' or not nets :
claimed = set ( )
else :
claimed = set ( nets . split ( ' , ' ) )
db [ relay ] [ " claim " ] = claimed
2015-10-24 03:29:10 +02:00
irc . reply ( ' CLAIM for channel \x02 %s \x02 set to: %s ' %
2015-09-26 18:39:45 +02:00
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )