2015-07-11 05:26:46 +02:00
# relay.py: PyLink Relay plugin
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
2017-06-30 08:10:53 +02:00
import inspect
2015-07-11 05:26:46 +02:00
2016-11-10 07:47:22 +01:00
from pylinkirc import utils , world , conf , structures
2016-06-21 03:18:54 +02:00
from pylinkirc . log import log
2016-11-08 06:18:20 +01:00
from pylinkirc . coremods import permissions
2015-07-11 05:26:46 +02:00
2017-05-05 06:16:25 +02:00
# Sets the timeout to wait for as individual servers / the PyLink daemon to start up.
TCONDITION_TIMEOUT = 2
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 )
2017-09-23 22:58:15 +02:00
channels_init_in_progress = defaultdict ( threading . Event )
2016-03-26 00:39:06 +01:00
2018-03-03 05:22:31 +01:00
dbname = conf . get_database_name ( ' pylinkrelay ' )
2016-11-10 07:52:46 +01:00
datastore = structures . PickleDataStore ( ' pylinkrelay ' , dbname )
2016-11-10 07:47:22 +01:00
db = datastore . store
2015-12-07 02:13:47 +01:00
2016-11-08 06:18:20 +01:00
default_permissions = { " *!*@* " : [ ' relay.linked ' ] ,
" $ircop " : [ ' relay.create ' , ' relay.linkacl* ' ,
' relay.destroy ' , ' relay.link ' , ' relay.delink ' ,
' relay.claim ' ] }
2015-09-15 02:23:56 +02:00
### INTERNAL FUNCTIONS
2016-12-10 02:57:02 +01:00
def initialize_all ( irc ) :
2015-09-15 02:23:56 +02:00
""" 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.
2017-05-05 06:16:25 +02:00
if world . started . wait ( TCONDITION_TIMEOUT ) :
for chanpair , entrydata in db . items ( ) :
network , channel = chanpair
2017-07-07 23:10:06 +02:00
# Initialize all channels that are relevant to the called network (i.e. channels either hosted there or a relay leaf channels)
if network == irc . name :
initialize_channel ( irc , channel )
2017-05-05 06:16:25 +02:00
for link in entrydata [ ' links ' ] :
network , channel = link
2017-07-07 23:10:06 +02:00
if network == irc . name :
initialize_channel ( irc , channel )
2015-09-15 02:23:56 +02:00
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. """
2016-05-01 23:09:17 +02:00
log . debug ( ' relay.main: loading links database ' )
2016-11-10 07:47:22 +01:00
datastore . load ( )
2016-05-01 23:09:17 +02:00
2017-08-02 16:24:23 +02:00
permissions . add_default_permissions ( default_permissions )
2016-11-08 06:18:20 +01:00
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 ( ) :
2016-12-10 02:57:02 +01:00
initialize_all ( ircobj )
2016-05-01 23:09:17 +02:00
log . debug ( ' relay.main: finished initialization sequence ' )
2015-09-27 20:56:09 +02:00
2017-05-13 04:19:52 +02:00
def die ( irc = None ) :
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:
2017-05-13 04:19:52 +02:00
for ircobj in world . networkobjects . values ( ) :
2016-07-24 20:13:51 +02:00
# 1) SQUIT every relay subserver.
2017-05-13 04:19:52 +02:00
for server , sobj in ircobj . servers . copy ( ) . items ( ) :
2015-09-27 20:56:09 +02:00
if hasattr ( sobj , ' remote ' ) :
2017-05-13 04:19:52 +02:00
ircobj . proto . squit ( ircobj . 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-11-08 06:18:20 +01:00
# 3) Unload our permissions.
2017-08-02 16:24:23 +02:00
permissions . remove_default_permissions ( default_permissions )
2016-11-08 06:18:20 +01:00
2016-11-10 07:47:22 +01:00
# 4) Save the database and quit.
datastore . die ( )
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-12-10 02:57:02 +01:00
def normalize_nick ( irc , netname , nick , times_tagged = 0 , uid = ' ' ) :
2016-07-11 08:32:04 +02:00
"""
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 :
2017-08-02 15:57:48 +02:00
# Check the following options in order, before falling back to True:
# 1) servers::<netname>::relay_tag_nicks
# 2) relay::tag_nicks
if irc . serverdata . get ( ' relay_tag_nicks ' , conf . conf . get ( ' relay ' , { } ) . get ( ' tag_nicks ' , True ) ) :
2016-07-27 01:01:42 +02:00
times_tagged = 1
else :
2017-12-30 10:22:24 +01:00
forcetag_nicks = set ( conf . conf . get ( ' relay ' , { } ) . get ( ' forcetag_nicks ' , [ ] ) )
forcetag_nicks | = set ( irc . serverdata . get ( ' relay_forcetag_nicks ' , [ ] ) )
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.normalize_nick: checking if globs %s match %s . ' , irc . name , forcetag_nicks , nick )
2016-07-27 01:01:42 +02:00
for glob in forcetag_nicks :
2017-06-30 08:01:39 +02:00
if irc . match_host ( glob , nick ) :
2016-07-27 01:01:42 +02:00
# User matched a nick to force tag nicks for. Tag them.
times_tagged = 1
break
2016-07-11 08:32:04 +02:00
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.normalize_nick: using %r as separator. ' , irc . name , separator )
2015-07-12 23:02:17 +02:00
orig_nick = nick
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.
2017-06-30 08:01:39 +02:00
protocol_allows_slashes = irc . has_cap ( ' slash-in-nicks ' ) 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 )
2017-06-30 08:01:39 +02:00
while irc . nick_to_uid ( nick ) and irc . nick_to_uid ( 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
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.normalize_nick: nick %r is in use; incrementing times tagged to %s . ' ,
2016-07-11 08:32:04 +02:00
irc . name , nick , times_tagged )
2016-12-10 02:57:02 +01:00
nick = normalize_nick ( 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
2016-12-10 02:57:02 +01:00
def normalize_host ( irc , host ) :
2015-11-22 08:37:19 +01:00
""" Creates a normalized hostname for the given host suitable for
introduction to a remote network ( as a relay client ) . """
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.normalize_host: IRCd= %s , host= %s ' , irc . name , irc . protoname , host )
2016-12-19 08:56:47 +01:00
allowed_chars = string . ascii_letters + string . digits + ' -.: '
2017-06-30 08:01:39 +02:00
if irc . has_cap ( ' slash-in-hosts ' ) :
2016-12-19 08:56:47 +01:00
# UnrealIRCd and IRCd-Hybrid don't allow slashes in hostnames
allowed_chars + = ' / '
2017-06-30 08:01:39 +02:00
if irc . has_cap ( ' underscore-in-hosts ' ) :
2017-03-24 07:12:59 +01:00
# Most IRCds allow _ in hostnames, but hybrid/charybdis/ratbox IRCds do not.
2016-12-19 09:00:24 +01:00
allowed_chars + = ' _ '
2016-12-18 08:47:26 +01:00
for char in host :
2016-12-19 08:56:47 +01:00
if char not in allowed_chars :
2016-12-18 08:47:26 +01:00
host = host . replace ( char , ' - ' )
2016-09-20 15:58:04 +02:00
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
2016-12-10 02:57:02 +01:00
def get_prefix_modes ( irc , remoteirc , channel , user , mlist = None ) :
2015-09-13 22:48:14 +02:00
"""
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
2017-09-03 06:35:03 +02:00
if channel in irc . channels and 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.
2017-10-15 10:28:21 +02:00
for pmode in irc . channels [ channel ] . get_prefix_modes ( 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-12-10 02:57:02 +01:00
def spawn_relay_server ( irc , remoteirc ) :
2017-09-24 05:40:54 +02:00
"""
Spawns a relay server representing " remoteirc " on " irc " .
"""
2017-08-31 21:27:52 +02:00
if irc . connected . is_set ( ) :
2017-05-05 06:16:25 +02:00
try :
2017-07-12 23:10:36 +02:00
suffix = irc . serverdata . get ( ' server_suffix ' , conf . conf . get ( ' relay ' , { } ) . get ( ' server_suffix ' , ' relay ' ) )
2017-05-05 06:16:25 +02:00
# Strip any leading or trailing .'s
suffix = suffix . strip ( ' . ' )
2018-02-11 00:31:12 +01:00
# On some IRCds (e.g. InspIRCd), we have to delay endburst to prevent triggering
# join flood protections that are counted locally.
needs_delayed_eob = hasattr ( irc , ' _endburst_delay ' )
if needs_delayed_eob :
old_eob_delay = irc . _endburst_delay
2018-02-11 00:53:49 +01:00
irc . _endburst_delay = irc . serverdata . get ( ' relay_endburst_delay ' , 10 )
2018-02-11 00:31:12 +01:00
2017-07-01 06:30:20 +02:00
sid = irc . spawn_server ( ' %s . %s ' % ( remoteirc . name , suffix ) ,
2018-02-11 00:31:12 +01:00
desc = " PyLink Relay network - %s " %
( remoteirc . get_full_network_name ( ) ) )
# Set _endburst_delay back to its last value.
if needs_delayed_eob :
irc . _endburst_delay = old_eob_delay
2017-05-05 06:16:25 +02:00
except ( RuntimeError , ValueError ) : # Network not initialized yet, or a server name conflict.
log . exception ( ' ( %s ) Failed to spawn server for %r (possible jupe?): ' ,
irc . name , remoteirc . name )
# We will just bail here. Disconnect the bad network.
irc . disconnect ( )
return
2016-05-01 08:59:51 +02:00
2017-05-05 06:16:25 +02:00
# Mark the server as a relay server
irc . servers [ sid ] . remote = remoteirc . name
2016-05-01 08:59:51 +02:00
2017-05-05 06:16:25 +02:00
# Assign the newly spawned server as our relay server for the target net.
relayservers [ irc . name ] [ remoteirc . name ] = sid
2016-05-01 08:59:51 +02:00
2017-05-05 06:16:25 +02:00
return sid
else :
2017-07-07 22:32:04 +02:00
log . debug ( ' ( %s ) skipping spawn_relay_server( %s , %s ); the local server ( %s ) is not ready yet ' ,
irc . name , irc . name , remoteirc . name , irc . name )
log . debug ( ' ( %s ) spawn_relay_server: current thread is %s ' ,
irc . name , threading . current_thread ( ) . name )
2016-05-01 08:59:51 +02:00
2017-09-24 05:40:54 +02:00
def get_relay_server_sid ( irc , remoteirc , spawn_if_missing = True ) :
"""
Fetches the relay server SID representing remoteirc on irc , spawning
a new server if it doesn ' t exist and spawn_if_missing is enabled.
"""
2015-11-16 06:42:58 +01:00
2017-06-30 08:10:53 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks_servers[ %s ] from thread %r in function %r ' , irc . name , irc . name ,
threading . current_thread ( ) . name , inspect . currentframe ( ) . f_code . co_name )
2017-05-05 06:16:25 +02:00
if spawnlocks_servers [ irc . name ] . acquire ( timeout = TCONDITION_TIMEOUT ) :
2015-09-12 21:06:58 +02:00
try :
sid = relayservers [ irc . name ] [ remoteirc . name ]
except KeyError :
2017-01-30 05:20:39 +01:00
if not spawn_if_missing :
2017-09-24 05:40:54 +02:00
log . debug ( ' ( %s ) get_relay_server_sid: %s .relay doesn \' t have a known SID, ignoring. ' , irc . name , remoteirc . name )
2017-01-30 05:20:39 +01:00
spawnlocks_servers [ irc . name ] . release ( )
return
2017-09-24 05:40:54 +02:00
log . debug ( ' ( %s ) get_relay_server_sid: %s .relay doesn \' t have a known SID, spawning. ' , irc . name , remoteirc . name )
2016-12-10 02:57:02 +01:00
sid = spawn_relay_server ( irc , remoteirc )
2016-05-01 08:59:51 +02:00
2017-09-24 05:40:54 +02:00
log . debug ( ' ( %s ) get_relay_server_sid: got %s for %s .relay ' , irc . name , sid , remoteirc . name )
2016-05-01 08:59:51 +02:00
if sid not in irc . servers :
2017-09-24 05:40:54 +02:00
log . warning ( ' ( %s ) Possible desync? SID %s for %s .relay doesn \' t exist anymore ' , irc . name , sid , remoteirc . name )
2016-05-01 08:59:51 +02:00
# Our stored server doesn't exist anymore. This state is probably a holdover from a netsplit,
# so let's refresh it.
2016-12-10 02:57:02 +01:00
sid = spawn_relay_server ( irc , remoteirc )
2016-05-01 08:59:51 +02:00
elif sid in irc . servers and irc . servers [ sid ] . remote != remoteirc . name :
2017-09-24 05:40:54 +02:00
log . debug ( ' ( %s ) Possible desync? SID %s for %s .relay doesn \' t exist anymore is mismatched (got %s .relay) ' , irc . name , irc . servers [ sid ] . remote , remoteirc . name )
2016-12-10 02:57:02 +01:00
sid = spawn_relay_server ( irc , remoteirc )
2016-05-01 08:59:51 +02:00
2017-09-24 05:40:54 +02:00
log . debug ( ' ( %s ) get_relay_server_sid: 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-12-10 02:57:02 +01:00
def spawn_relay_user ( irc , remoteirc , user , times_tagged = 0 ) :
2017-09-24 05:40:54 +02:00
"""
Spawns a relay user representing " user " from " irc " ( the local network ) on remoteirc ( the target network ) .
"""
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-12-10 02:57:02 +01:00
nick = normalize_nick ( 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
2016-12-10 02:57:02 +01:00
host = normalize_host ( remoteirc , userobj . host )
2016-05-01 08:59:51 +02:00
realname = userobj . realname
2016-12-10 02:57:02 +01:00
modes = set ( get_supported_umodes ( irc , remoteirc , userobj . modes ) )
2016-05-01 08:59:51 +02:00
opertype = ' '
if ( ' o ' , None ) in userobj . modes :
2017-02-18 22:51:45 +01:00
# Try to get the oper type, adding an "(on <networkname>)" suffix similar to what
# Janus does.
2016-05-01 08:59:51 +02:00
if hasattr ( userobj , ' opertype ' ) :
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.get_remote_user: setting OPERTYPE of client for %r to %s ' ,
2016-05-01 08:59:51 +02:00
irc . name , user , userobj . opertype )
2017-02-18 22:51:45 +01:00
opertype = userobj . opertype
2016-05-01 08:59:51 +02:00
else :
2017-02-18 22:51:45 +01:00
opertype = ' IRC Operator '
2017-06-30 08:01:39 +02:00
opertype + = ' (on %s ) ' % irc . get_full_network_name ( )
2017-02-18 22:51:45 +01:00
2016-05-01 08:59:51 +02:00
# 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 ) )
2017-09-24 05:40:54 +02:00
rsid = get_relay_server_sid ( remoteirc , irc )
2016-11-08 06:47:53 +01:00
if not rsid :
2016-12-10 02:57:02 +01:00
log . error ( ' ( %s ) spawn_relay_user: aborting user spawn for %s / %s @ %s (failed to retrieve a '
2016-11-08 07:04:22 +01:00
' working SID). ' , irc . name , user , nick , remoteirc . name )
2016-11-08 06:47:53 +01:00
return
2016-05-01 08:59:51 +02:00
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
2017-07-01 06:30:20 +02:00
u = remoteirc . spawn_client ( nick , ident = ident , host = host , realname = realname , modes = modes ,
2016-07-11 07:25:01 +02:00
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 :
2017-06-25 11:07:24 +02:00
remoteirc . away ( u , away )
2016-07-02 03:54:07 +02:00
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
2017-01-30 07:18:05 +01:00
def get_remote_user ( irc , remoteirc , user , spawn_if_missing = True , times_tagged = 0 ) :
2016-05-01 23:21:14 +02:00
"""
2017-09-24 05:40:54 +02:00
Fetches and returns the relay client UID representing " user " on the remote network " remoteirc " ,
spawning a new user if one doesn ' t exist and spawn_if_missing 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.
2017-08-31 21:27:52 +02:00
if irc . connected . is_set ( ) :
2017-05-05 06:16:25 +02:00
# Don't spawn clones for registered service bots.
2017-06-30 08:01:39 +02:00
sbot = irc . get_service_bot ( user )
2017-05-05 06:16:25 +02:00
if sbot :
return sbot . uids . get ( remoteirc . name )
2017-06-30 08:10:53 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] from thread %r in function %r ' , irc . name , irc . name ,
threading . current_thread ( ) . name , inspect . currentframe ( ) . f_code . co_name )
2017-05-05 06:16:25 +02:00
if spawnlocks [ irc . name ] . acquire ( timeout = TCONDITION_TIMEOUT ) :
# Be sort-of thread safe: lock the user spawns for the current net first.
u = None
try :
# Look up the existing user, stored here as dict entries in the format:
# {('ournet', 'UID'): {'remotenet1': 'UID1', 'remotenet2': 'UID2'}}
u = relayusers [ ( irc . name , user ) ] [ remoteirc . name ]
except KeyError :
# User doesn't exist. Spawn a new one if requested.
if spawn_if_missing :
u = spawn_relay_user ( irc , remoteirc , user , times_tagged = times_tagged )
# 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.
2017-09-24 05:40:54 +02:00
# XXX: this technically means that PyLink is desyncing somewhere, and that we should
# fix this in core properly...
2017-05-05 06:16:25 +02:00
if u and ( ( u not in remoteirc . users ) or remoteirc . users [ u ] . remote != ( irc . name , user ) ) :
2017-09-24 05:40:54 +02:00
log . warning ( ' ( %s ) Possible desync? Got invalid relay UID %s for %s on %s ' ,
irc . name , u , irc . get_friendly_name ( user ) , remoteirc . name )
2016-12-10 02:57:02 +01:00
u = spawn_relay_user ( irc , remoteirc , user , times_tagged = times_tagged )
2015-11-22 22:08:31 +01:00
2017-05-05 06:16:25 +02:00
spawnlocks [ irc . name ] . release ( )
2016-07-02 03:54:35 +02:00
2017-05-05 06:16:25 +02:00
return u
else :
2017-07-07 22:32:04 +02:00
log . debug ( ' ( %s ) skipping spawn_relay_user( %s , %s , %s , ...); the local server ( %s ) is not ready yet ' ,
irc . name , irc . name , remoteirc . name , user , irc . name )
log . debug ( ' ( %s ) spawn_relay_user: current thread is %s ' ,
irc . name , threading . current_thread ( ) . name )
2015-07-14 21:04:05 +02:00
2016-12-10 02:57:02 +01:00
def get_orig_user ( 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
2016-12-10 02:57:02 +01:00
If targetirc is given , get_remote_user ( ) is called to get the relay client
2015-09-15 02:23:56 +02:00
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-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.get_orig_user: 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 ]
2016-12-10 02:57:02 +01:00
# Otherwise, use get_remote_user to find our UID.
res = get_remote_user ( sourceobj , targetirc , remoteuser [ 1 ] ,
2017-01-30 07:18:05 +01:00
spawn_if_missing = False )
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.get_orig_user: targetirc found as %s , getting %r as '
2016-07-08 07:16:34 +02:00
' 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
2017-08-18 21:39:47 +02:00
def get_relay ( irc , channel ) :
""" Finds the matching relay entry name for the given network, channel
2015-09-17 05:59:08 +02:00
pair , if one exists . """
2017-08-18 21:39:47 +02:00
chanpair = ( irc . name , irc . to_lower ( channel ) )
2017-05-05 03:50:42 +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
2016-12-10 02:57:02 +01:00
def get_remote_channel ( 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
remotenetname = remoteirc . name
2017-08-18 21:39:47 +02:00
chanpair = get_relay ( irc , channel )
2015-07-14 04:46:24 +02:00
if chanpair is None :
return
2017-08-18 21:39:47 +02:00
2015-07-14 04:46:24 +02:00
if chanpair [ 0 ] == remotenetname :
return chanpair [ 1 ]
else :
2017-05-05 03:50:42 +02:00
for link in db [ chanpair ] [ ' links ' ] :
if link [ 0 ] == remotenetname :
return link [ 1 ]
2015-07-14 04:46:24 +02:00
2016-12-10 02:57:02 +01:00
def initialize_channel ( irc , channel ) :
2015-09-15 02:23:56 +02:00
""" Initializes a relay channel (merge local/remote users, set modes, etc.). """
2017-09-23 22:58:15 +02:00
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.
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , channel )
2017-11-12 20:56:33 +01:00
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.initialize_channel being called on %s ' , irc . name , channel )
log . debug ( ' ( %s ) relay.initialize_channel: 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 :
2017-11-12 20:56:33 +01:00
all_links = db [ relay ] [ ' links ' ] . copy ( )
all_links . update ( ( relay , ) )
log . debug ( ' ( %s ) relay.initialize_channel: all_links: %s ' , irc . name , all_links )
# Iterate over all the remote channels linked in this relay.
for link in all_links :
remotenet , remotechan = link
if remotenet == irc . name : # If the network is us, skip.
continue
remoteirc = world . networkobjects . get ( remotenet )
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
if remoteirc is None :
# Remote network doesn't have an IRC object; e.g. it was removed
# from the config. Skip this.
continue
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
# Give each network a tiny bit of leeway to finish up its connection.
# This is better than just dropping users their completely.
if not remoteirc . connected . wait ( TCONDITION_TIMEOUT ) :
continue
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
# Join their (remote) users and set their modes, if applicable.
if remotechan in remoteirc . channels :
rc = remoteirc . channels [ remotechan ]
'''
if not hasattr ( rc , ' _relay_initial_burst ' ) :
rc . _relay_initial_burst = threading . Event ( )
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
if rc . _relay_initial_burst . is_set ( ) :
log . debug ( ' ( %s ) relay.initialize_channel: skipping inbound burst from %s / %s => %s / %s '
' as it has already been bursted ' , irc . name , remoteirc . name , remotechan , irc . name , channel )
continue
rc . _relay_initial_burst . set ( )
'''
relay_joins ( remoteirc , remotechan , rc . users , rc . ts , targetirc = irc )
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
# Only update the topic if it's different from what we already have,
# and topic bursting is complete.
if rc . topicset and rc . topic != irc . channels [ channel ] . topic :
irc . topic_burst ( irc . sid , channel , rc . topic )
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
# Send our users and channel modes to the other nets
if channel in irc . channels :
c = irc . _channels [ channel ]
relay_joins ( irc , channel , c . users , c . ts )
2017-09-23 22:58:15 +02:00
2017-11-12 20:56:33 +01:00
if ' pylink ' in world . services :
world . services [ ' pylink ' ] . join ( irc , channel )
2015-07-14 21:04:05 +02:00
2016-12-10 02:57:02 +01:00
def remove_channel ( irc , channel ) :
2015-09-15 02:23:56 +02:00
""" 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-12-10 02:34:34 +01:00
if irc . pseudoclient :
2017-06-25 11:07:24 +02:00
irc . part ( irc . pseudoclient . uid , channel , ' Channel delinked. ' )
2016-03-05 18:31:59 +01:00
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , channel )
2017-08-25 11:29:10 +02:00
if relay and channel in irc . channels :
2015-09-15 02:23:56 +02:00
for user in irc . channels [ channel ] . users . copy ( ) :
2017-07-11 07:38:25 +02:00
if not is_relay_client ( irc , user ) :
2016-12-10 02:57:02 +01:00
relay_part ( irc , channel , user )
2015-09-15 02:23:56 +02:00
# 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
2017-06-25 11:07:24 +02:00
irc . 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 :
2016-12-10 02:57:02 +01:00
remoteuser = get_orig_user ( irc , user )
2015-09-15 02:23:56 +02:00
del relayusers [ remoteuser ] [ irc . name ]
2017-06-25 11:07:24 +02:00
irc . quit ( user , ' Left all shared channels. ' )
2015-07-15 04:39:49 +02:00
2016-12-10 02:57:02 +01:00
def check_claim ( 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 .
2017-08-11 22:15:03 +02:00
5 ) The sender is halfop or above in the channel but NOT a U - line
( this is because we allow u - lines to override with ops to prevent mode floods ) .
2015-09-17 05:59:08 +02:00
6 ) The sender is a PyLink client / server ( checks are suppressed in this case ) .
2015-09-13 07:50:53 +02:00
"""
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , 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
2016-12-10 02:57:02 +01:00
sender_modes = get_prefix_modes ( irc , irc , channel , sender , mlist = mlist )
log . debug ( ' ( %s ) relay.check_claim: sender modes ( %s / %s ) are %s (mlist= %s ) ' , irc . name ,
2015-09-13 22:48:14 +02:00
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
2017-05-05 03:50:42 +02:00
return ( not relay ) or irc . name == relay [ 0 ] or not db [ relay ] [ ' claim ' ] or \
irc . name in db [ relay ] [ ' claim ' ] or \
2017-08-11 22:15:03 +02:00
( any ( [ mode in sender_modes for mode in ( ' y ' , ' q ' , ' a ' , ' o ' , ' h ' ) ] )
and not _is_uline ( irc , sender ) ) \
2017-06-30 08:01:39 +02:00
or irc . is_internal_client ( sender ) or \
irc . is_internal_server ( sender )
2015-09-13 07:50:53 +02:00
2016-12-10 02:57:02 +01:00
def get_supported_umodes ( irc , remoteirc , modes ) :
2015-09-15 02:23:56 +02:00
""" 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 :
2017-12-18 22:23:16 +01:00
if name not in WHITELISTED_UMODES :
2016-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_umodes: 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-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_umodes: 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
2017-07-11 07:38:25 +02:00
def is_relay_client ( irc , user ) :
2015-09-15 02:23:56 +02:00
""" 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
2017-07-11 07:38:25 +02:00
isRelayClient = is_relay_client
2015-09-15 02:23:56 +02:00
2017-07-11 08:09:00 +02:00
def iterate_all ( origirc , func , extra_args = ( ) , kwargs = None ) :
"""
2017-08-31 23:19:30 +02:00
Runs the given function ' func ' on all connected networks . ' func ' must take at least two arguments : the original network object and the remote network object .
2017-07-11 08:09:00 +02:00
"""
if kwargs is None :
kwargs = { }
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
if name == origirc . name or not remoteirc . connected . is_set ( ) :
# Don't relay things to their source network...
continue
func ( origirc , remoteirc , * extra_args , * * kwargs )
2017-08-31 23:19:30 +02:00
def iterate_all_present ( origirc , origuser , func , extra_args = ( ) , kwargs = None ) :
"""
Runs the given function ' func ' on all networks where the UID ' origuser '
from ' origirc ' has a relay client .
' func ' must take at least three arguments : the original network object ,
the remote network object , and the UID on the remote network to work on .
"""
if kwargs is None :
kwargs = { }
for netname , user in relayusers [ ( origirc . name , origuser ) ] . copy ( ) . items ( ) :
remoteirc = world . networkobjects [ netname ]
func ( origirc , remoteirc , user , * extra_args , * * kwargs )
2015-09-15 02:23:56 +02:00
### EVENT HANDLER INTERNALS
2017-11-12 20:56:33 +01:00
def relay_joins ( irc , channel , users , ts , targetirc = None , * * kwargs ) :
2016-02-28 03:36:20 +01:00
"""
2017-11-12 20:56:33 +01:00
Relays one or more users ' joins from a channel to its relay links. If targetirc is given, only burst
to that specific network .
2016-02-28 03:36:20 +01:00
"""
2017-01-07 07:48:11 +01:00
if ts < 750000 :
current_ts = int ( time . time ( ) )
log . debug ( ' ( %s ) relay: resetting too low TS value of %s on %s to %s ' , irc . name , ts , users , current_ts )
ts = current_ts
2017-07-11 08:09:00 +02:00
def _relay_joins_loop ( irc , remoteirc , channel , users , ts , burst = True ) :
2015-09-15 02:23:56 +02:00
queued_users = [ ]
2016-02-28 03:36:20 +01:00
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( 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,
2017-07-11 08:09:00 +02:00
# just skip it.
return
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
for user in users . copy ( ) :
2017-07-11 07:38:25 +02:00
if is_relay_client ( 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
2016-12-10 02:57:02 +01:00
assert user in irc . users , " ( %s ) relay.relay_joins: How is this possible? %r isn ' t in our user database. " % ( irc . name , user )
u = get_remote_user ( 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
2017-08-25 11:29:10 +02:00
if ( remotechan not in remoteirc . channels ) or 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.
2017-06-30 08:01:39 +02:00
if not irc . has_cap ( ' has-ts ' ) :
2016-09-26 04:28:15 +02:00
# Special hack for clientbot: just use the remote's modes so mode changes
2017-04-09 23:49:19 +02:00
# take precendence. (TS is always outside the clientbot's control)
2017-08-31 06:08:54 +02:00
if remotechan in remoteirc . channels :
ts = remoteirc . channels [ remotechan ] . ts
else :
ts = int ( time . time ( ) )
2016-09-26 04:28:15 +02:00
else :
ts = irc . channels [ channel ] . ts
2016-12-10 02:57:02 +01:00
prefixes = get_prefix_modes ( 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.
2017-09-24 05:40:54 +02:00
rsid = get_relay_server_sid ( remoteirc , irc )
2015-09-15 02:23:56 +02:00
if burst or len ( queued_users ) > 1 or queued_users [ 0 ] [ 0 ] :
2016-12-10 02:57:02 +01:00
modes = get_supported_cmodes ( irc , remoteirc , channel , irc . channels [ channel ] . modes )
2017-07-11 07:36:43 +02:00
2017-04-01 08:20:27 +02:00
# Subtract any mode delta modes from this burst
2017-12-14 20:56:41 +01:00
relay = db [ get_relay ( irc , channel ) ]
2017-04-01 08:20:27 +02:00
modedelta_modes = relay . get ( ' modedelta ' )
if modedelta_modes :
2017-12-14 20:56:41 +01:00
# Check if the target is a leaf channel: if so, add the mode delta modes to the target channel.
# Otherwise, subtract this set of modes, as we don't want these modes from leaves to be sent back
# to the original channel.
adding = ( remoteirc . name , remotechan ) in relay [ ' links ' ]
2017-04-01 08:20:27 +02:00
# Add this to the SJOIN mode list.
for mode in modedelta_modes :
modechar = remoteirc . cmodes . get ( mode [ 0 ] )
2017-12-14 22:07:00 +01:00
2017-04-01 08:20:27 +02:00
if modechar :
2017-12-14 22:07:00 +01:00
if modechar in remoteirc . cmodes [ ' *A ' ] or modechar in remoteirc . prefixmodes :
log . warning ( ' ( %s ) Refusing to set modedelta mode %r on %s because it is a list or prefix mode ' ,
irc . name , modechar , channel )
continue
2017-12-14 22:26:43 +01:00
elif not remoteirc . has_cap ( ' can-spawn-clients ' ) :
log . debug ( ' ( %s ) relay.handle_mode: Not enforcing modedelta modes on bot-only network %s ' ,
irc . name , remoteirc . name )
continue
2017-12-14 22:07:00 +01:00
2017-04-01 08:20:27 +02:00
modedelta_mode = ( ' + %s ' % modechar , mode [ 1 ] )
if adding :
2017-12-14 20:56:41 +01:00
log . debug ( ' ( %s ) relay.relay_joins: adding %r on %s / %s (modedelta) ' , irc . name ,
str ( modedelta_mode ) , remoteirc . name , remotechan )
2017-04-01 08:20:27 +02:00
modes . append ( modedelta_mode )
elif modedelta_mode in modes :
2017-12-14 20:56:41 +01:00
log . debug ( ' ( %s ) relay.relay_joins: removing %r on %s / %s (modedelta) ' , irc . name ,
str ( modedelta_mode ) , remoteirc . name , remotechan )
2017-04-01 08:20:27 +02:00
modes . remove ( modedelta_mode )
2016-11-08 06:47:53 +01:00
if rsid :
2017-06-25 11:07:24 +02:00
remoteirc . sjoin ( rsid , 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.
2017-06-25 11:07:24 +02:00
remoteirc . join ( queued_users [ 0 ] [ 1 ] , remotechan )
2015-07-20 08:49:50 +02:00
2017-07-11 07:36:43 +02:00
remoteirc . call_hooks ( [ rsid , ' PYLINK_RELAY_JOIN ' , { ' channel ' : remotechan , ' users ' : [ u [ - 1 ] for u in queued_users ] } ] )
2016-07-08 06:38:12 +02:00
2017-11-12 20:56:33 +01:00
if targetirc :
_relay_joins_loop ( irc , targetirc , channel , users , ts , * * kwargs )
else :
iterate_all ( irc , _relay_joins_loop , extra_args = ( channel , users , ts ) , kwargs = kwargs )
2017-07-11 08:09:00 +02:00
def relay_part ( irc , * args , * * kwargs ) :
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 .
"""
2017-07-11 08:09:00 +02:00
def _relay_part_loop ( irc , remoteirc , channel , user ) :
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( irc , remoteirc , channel )
log . debug ( ' ( %s ) relay.relay_part: looking for %s / %s on %s ' , irc . name , user , irc . name , remoteirc . name )
log . debug ( ' ( %s ) relay.relay_part: remotechan found as %s ' , irc . name , remotechan )
2016-02-28 03:36:20 +01:00
2017-01-30 07:18:05 +01:00
remoteuser = get_remote_user ( irc , remoteirc , user , spawn_if_missing = False )
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.relay_part: remoteuser for %s / %s found as %s ' , irc . name , user , irc . name , remoteuser )
2016-02-28 03:36:20 +01:00
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.
2017-07-11 08:09:00 +02:00
return
2016-02-28 03:36:20 +01:00
# Part the relay client with the channel delinked message.
2017-06-25 11:07:24 +02:00
remoteirc . 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.
2017-07-11 07:38:25 +02:00
if is_relay_client ( remoteirc , remoteuser ) and not remoteirc . users [ remoteuser ] . channels :
2017-06-25 11:07:24 +02:00
remoteirc . 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
2017-07-11 08:09:00 +02:00
iterate_all ( irc , _relay_part_loop , extra_args = args , kwargs = kwargs )
2015-07-16 20:53:40 +02:00
2017-12-18 22:23:16 +01:00
WHITELISTED_CMODES = {
2017-08-11 22:29:45 +02:00
' admin ' ,
' adminonly ' ,
' allowinvite ' ,
' autoop ' ,
' ban ' ,
' banexception ' ,
' blockcolor ' ,
2017-12-05 21:01:57 +01:00
' blockcaps ' ,
2017-12-22 06:18:20 +01:00
' blockhighlight ' ,
2017-12-05 21:01:57 +01:00
' exemptchanops ' ,
' filter ' ,
2017-08-11 22:29:45 +02:00
' flood ' ,
' flood_unreal ' ,
' freetarget ' ,
' halfop ' ,
2017-12-05 21:01:57 +01:00
' hidequits ' ,
' history ' ,
2017-08-11 22:29:45 +02:00
' invex ' ,
' inviteonly ' ,
' joinflood ' ,
' key ' ,
2017-12-18 22:16:38 +01:00
' kicknorejoin ' ,
' kicknorejoin_insp ' ,
2017-12-05 21:01:57 +01:00
' largebanlist ' ,
2017-08-11 22:29:45 +02:00
' limit ' ,
' moderated ' ,
2017-08-11 22:32:04 +02:00
' nickflood ' ,
2017-12-05 21:01:57 +01:00
' noamsg ' ,
2017-08-11 22:29:45 +02:00
' noctcp ' ,
' noextmsg ' ,
' noforwards ' ,
' noinvite ' ,
' nokick ' ,
' noknock ' ,
' nonick ' ,
' nonotice ' ,
' op ' ,
' operonly ' ,
' opmoderated ' ,
' owner ' ,
' private ' ,
' quiet ' ,
' regmoderated ' ,
' regonly ' ,
2017-12-18 22:16:38 +01:00
' repeat ' ,
' repeat_insp ' ,
2017-08-11 22:29:45 +02:00
' secret ' ,
' sslonly ' ,
' stripcolor ' ,
' topiclock ' ,
' voice '
}
2017-12-18 22:23:16 +01:00
WHITELISTED_UMODES = {
2017-08-11 22:29:45 +02:00
' bot ' ,
' hidechans ' ,
' hideidle ' ,
' hideoper ' ,
' invisible ' ,
' noctcp ' ,
' oper ' ,
' regdeaf ' ,
' stripcolor ' ,
' wallops '
}
2017-12-18 22:23:16 +01:00
CLIENTBOT_WHITELISTED_UMODES = { ' admin ' , ' ban ' , ' banexception ' , ' halfop ' , ' invex ' , ' op ' , ' owner ' , ' voice ' }
CLIENTBOT_MODESYNC_OPTIONS = ( ' none ' , ' half ' , ' full ' )
2016-12-10 02:57:02 +01:00
def get_supported_cmodes ( 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
"""
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( 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
2016-09-26 05:06:24 +02:00
# Handle Clientbot-specific mode whitelist settings
2017-12-18 22:23:16 +01:00
whitelist = WHITELISTED_CMODES
2016-09-26 05:06:24 +02:00
if remoteirc . protoname == ' clientbot ' or irc . protoname == ' clientbot ' :
modesync = conf . conf . get ( ' relay ' , { } ) . get ( ' clientbot_modesync ' , ' none ' ) . lower ( )
2017-12-18 22:23:16 +01:00
if modesync not in CLIENTBOT_MODESYNC_OPTIONS :
2016-09-26 05:06:24 +02:00
modesync = ' none '
log . warning ( ' relay: Bad clientbot_modesync option %s : valid values are %s ' ,
2017-12-18 22:23:16 +01:00
modesync , CLIENTBOT_MODESYNC_OPTIONS )
2016-09-26 05:06:24 +02:00
if modesync == ' none ' :
return [ ] # Do nothing
elif modesync == ' half ' :
2017-12-18 22:23:16 +01:00
whitelist = CLIENTBOT_WHITELISTED_UMODES
2016-09-26 05:06:24 +02: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 ( ) :
2017-08-06 06:52:52 +02:00
mode_parse_aborted = False
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 :
2017-08-06 07:02:12 +02:00
if name not in whitelist :
log . debug ( " ( %s ) relay.get_supported_cmodes: skipping mode ( %r , %r ) because "
" it isn ' t a whitelisted (safe) mode for relay. " ,
irc . name , modechar , arg )
break
2015-07-22 08:53:29 +02:00
supported_char = remoteirc . cmodes . get ( name )
2017-08-06 06:52:52 +02:00
2017-08-24 06:18:44 +02:00
# The mode we requested is an acting extban on the target network.
# Basically there are 3 possibilities when handling these extban-like modes:
2017-08-13 02:12:56 +02:00
# 1) Both target & source both use a chmode (e.g. ts6 +q). In these cases, the mode is just forwarded as-is.
# 2) Forwarding from chmode to extban - this is the case being handled here.
# 3) Forwarding from extban to extban (see below)
2017-08-24 07:29:14 +02:00
pending_extban_prefixes = [ ]
2017-08-06 06:52:52 +02:00
if name in remoteirc . extbans_acting :
# We make the assumption that acting extbans can only be used with +b...
old_arg = arg
supported_char = remoteirc . cmodes [ ' ban ' ]
2017-08-24 07:29:14 +02:00
pending_extban_prefixes . append ( name ) # Save the extban prefix for joining later
2017-08-24 08:23:02 +02:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: folding mode %s %s %s to %s %s %s %s for %s ' ,
irc . name , prefix , modechar , old_arg , prefix , supported_char ,
remoteirc . extbans_acting [ name ] , arg , remoteirc . name )
2017-08-06 06:52:52 +02:00
elif supported_char is None :
continue
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-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: 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
2017-06-30 08:01:39 +02:00
if ( not irc . has_cap ( ' can-spawn-clients ' ) ) and irc . pseudoclient and arg == irc . pseudoclient . uid :
2017-01-07 01:45:48 +01:00
# Skip modesync on the main PyLink client.
log . debug ( " ( %s ) relay.get_supported_cmodes: filtering prefix change ( %r , %r ) on Clientbot relayer " ,
irc . name , name , arg )
break
2015-07-22 08:47:06 +02:00
# If the target is a remote user, get the real target
# (original user).
2016-12-10 02:57:02 +01:00
arg = get_orig_user ( irc , arg , targetirc = remoteirc ) or \
2017-01-30 07:18:05 +01:00
get_remote_user ( irc , remoteirc , arg , spawn_if_missing = 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-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: argument found as ( %r , %r ) "
2015-07-22 08:47:06 +02:00
" for network %r . " ,
irc . name , modechar , arg , remoteirc . name )
2017-08-25 11:29:10 +02:00
oplist = [ ]
if remotechan in remoteirc . channels :
oplist = remoteirc . channels [ remotechan ] . prefixmodes [ name ]
2016-02-28 03:36:20 +01:00
2016-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: 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-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: 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
2017-08-24 06:18:44 +02:00
elif arg :
# Acting extban case 3: forwarding extban -> extban or mode
# First, we expand extbans from the local IRCd into a named mode and argument pair. Then, we
# can figure out how to relay it.
for extban_name , extban_prefix in irc . extbans_acting . items ( ) :
# Acting extbans are only supported with +b (e.g. +b m:n!u@h)
if name == ' ban ' and arg . startswith ( extban_prefix ) :
orig_supported_char , old_arg = supported_char , arg
if extban_name in remoteirc . cmodes :
# This extban is a mode on the target network. Chop off the extban prefix and set
# the mode character to the target's mode for it.
supported_char = remoteirc . cmodes [ extban_name ]
arg = arg [ len ( extban_prefix ) : ]
2017-08-24 08:23:02 +02:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: expanding acting extban %s %s %s to %s %s %s for %s ' ,
irc . name , prefix , orig_supported_char , old_arg , prefix ,
supported_char , arg , remoteirc . name )
2017-12-07 20:26:16 +01:00
# Override the mode name so that we're not overly strict about nick!user@host
# conformance. Note: the reverse (cmode->extban) is not done because that would
# also trigger the nick!user@host filter for +b.
name = extban_name
2017-08-24 06:18:44 +02:00
elif extban_name in remoteirc . extbans_acting :
# This is also an extban on the target network.
# Just chop off the local prefix now; we rewrite it later after processing
# any matching extbans.
2017-08-24 07:29:14 +02:00
pending_extban_prefixes . append ( extban_name )
2017-08-24 06:18:44 +02:00
arg = arg [ len ( extban_prefix ) : ]
2017-08-24 08:23:02 +02:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: expanding acting extban %s %s %s to %s %s %s %s for %s ' ,
irc . name , prefix , orig_supported_char , old_arg , prefix ,
supported_char , remoteirc . extbans_acting [ extban_name ] , arg ,
remoteirc . name )
2017-08-24 06:18:44 +02:00
else :
# This mode/extban isn't supported, so ignore it.
2017-08-24 08:23:02 +02:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: blocking acting extban '
' %s %s %s as target %s doesn \' t support it ' ,
2017-08-24 06:18:44 +02:00
irc . name , prefix , supported_char , arg , remoteirc . name )
mode_parse_aborted = True # XXX: nested loops are ugly...
break # Only one extban per mode pair, so break.
# Handle matching extbans such as Charybdis $a, UnrealIRCd ~a, InspIRCd R:, etc.
for extban_name , extban_prefix in irc . extbans_matching . items ( ) :
# For matching extbans, we check for the following:
# 1) arg == extban, for extbans like Charybdis $o and $a that are valid without an argument.
# 2) arg starting with extban, the most general case.
# Extbans with and without args have different mode names to prevent ambiguity and
# allow proper forwarding.
old_arg = arg
if arg == extban_prefix :
# This is a matching extban with no arg (case 1).
if extban_name in remoteirc . extbans_matching :
# Replace the ban with the remote's version entirely.
arg = remoteirc . extbans_matching [ extban_name ]
2017-12-07 20:04:26 +01:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: mangling static matching extban %s => %s for %s ' ,
2017-08-24 08:23:02 +02:00
irc . name , old_arg , arg , remoteirc . name )
2017-08-24 06:18:44 +02:00
break
else :
# Unsupported, don't forward it.
2017-12-07 20:04:26 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: setting mode_parse_aborted as ( %r , %r ) "
" (name= %r ; extban_name= %r ) doesn ' t match any (static) extban on %s " ,
irc . name , supported_char , arg , name , extban_name , remoteirc . name )
2017-08-24 06:18:44 +02:00
mode_parse_aborted = True
2017-12-07 20:05:39 +01:00
elif extban_prefix . endswith ( ' : ' ) and arg . startswith ( extban_prefix ) :
# This is a full extban with a prefix and some data. The assumption: all extbans with data
# have a prefix ending with : (as a delimiter)
2017-08-24 06:18:44 +02:00
if extban_name in remoteirc . extbans_matching :
# Chop off our prefix and apply the remote's.
arg = arg [ len ( extban_prefix ) : ]
arg = remoteirc . extbans_matching [ extban_name ] + arg
2017-08-24 08:23:02 +02:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: mangling matching extban arg %s => %s for %s ' ,
irc . name , old_arg , arg , remoteirc . name )
2017-08-24 06:18:44 +02:00
break
else :
2017-12-07 20:04:26 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: setting mode_parse_aborted as ( %r , %r ) "
" (name= %r ; extban_name= %r ) doesn ' t match any (dynamic) extban on %s " ,
irc . name , supported_char , arg , name , extban_name , remoteirc . name )
2017-08-24 06:18:44 +02:00
mode_parse_aborted = True
else :
2017-08-31 06:09:19 +02:00
if name in ( ' ban ' , ' banexception ' , ' invex ' , ' quiet ' ) and not remoteirc . is_hostmask ( arg ) :
2017-08-24 06:18:44 +02:00
# Don't add unsupported bans that don't match n!u@h syntax.
2017-12-07 20:26:16 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: skipping unsupported extban/mode ( %r , %r ) "
" because it doesn ' t match nick!user@host. (name= %r ) " ,
irc . name , supported_char , arg , name )
2017-08-24 06:18:44 +02:00
break
# We broke up an acting extban earlier. Now, rewrite it into a new mode by joining the prefix and data together.
2017-08-24 07:29:14 +02:00
while pending_extban_prefixes :
2017-12-07 20:04:26 +01:00
next_prefix = pending_extban_prefixes . pop ( )
2017-12-07 20:33:16 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: readding extban prefix %r ( %r ) to ( %r , %r ) for %s " ,
irc . name , next_prefix , remoteirc . extbans_acting [ next_prefix ] ,
supported_char , arg , remoteirc . name )
2017-12-07 20:04:26 +01:00
arg = remoteirc . extbans_acting [ next_prefix ] + arg
2017-08-06 06:52:52 +02:00
if mode_parse_aborted :
2017-12-07 20:04:26 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: blocking unsupported extban/mode ( %r , %r ) for %s (mode_parse_aborted) " ,
irc . name , supported_char , arg , remoteirc . name )
2017-08-06 06:52:52 +02:00
break
2015-07-25 19:43:47 +02:00
final_modepair = ( prefix + supported_char , arg )
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.
2017-08-25 11:29:10 +02:00
if prefix == ' + ' and ( remotechan not in remoteirc . channels or final_modepair in remoteirc . channels [ remotechan ] . modes ) :
2016-12-10 02:57:02 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: 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 )
2017-12-07 20:04:26 +01:00
log . debug ( " ( %s ) relay.get_supported_cmodes: added modepair ( %r , %r ) for %s %s " ,
irc . name , supported_char , arg , remoteirc . name , remotechan )
2017-08-06 06:13:26 +02:00
break
2016-02-28 03:36:20 +01:00
2016-12-10 02:57:02 +01:00
log . debug ( ' ( %s ) relay.get_supported_cmodes: final modelist (sending to %s %s ) is %s ' , irc . name , remoteirc . name , remotechan , supported_modes )
2016-06-23 04:49:49 +02:00
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 )
2017-06-25 11:07:24 +02:00
irc . numeric ( server , num , source , text )
2016-05-20 07:23:34 +02:00
2017-07-12 16:51:34 +02:00
def _check_send_key ( infoline ) :
2016-05-20 08:30:34 +02:00
"""
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
2017-06-30 08:01:39 +02:00
elif setting == ' opers ' and irc . is_oper ( source , allowAuthed = False ) :
2016-05-20 08:30:34 +02:00
return True
return False
2016-05-20 07:23:34 +02:00
# Get the real user for the WHOIS target.
2016-12-10 02:57:02 +01:00
origuser = get_orig_user ( irc , target )
2016-05-20 07:23:34 +02:00
if origuser :
homenet , uid = origuser
realirc = world . networkobjects [ homenet ]
realuser = realirc . users [ uid ]
2017-06-30 08:01:39 +02:00
netname = realirc . get_full_network_name ( )
2016-05-20 07:23:34 +02:00
wreply ( 320 , " :is a remote user connected via PyLink Relay. Home network: %s ; "
" Home nick: %s " % ( netname , realuser . nick ) )
2017-07-12 16:51:34 +02:00
if _check_send_key ( ' whois_show_accounts ' ) and realuser . services_account :
2016-05-20 08:30:34 +02:00
# 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 ) )
2017-07-12 16:51:34 +02:00
if _check_send_key ( ' whois_show_server ' ) and realirc . has_cap ( ' can-track-servers ' ) :
2016-05-20 08:30:34 +02:00
wreply ( 320 , " :is actually connected via the following server: " )
2017-06-30 08:01:39 +02:00
realserver = realirc . get_server ( uid )
2016-05-20 08:30:34 +02:00
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 .
"""
2017-06-30 08:01:39 +02:00
newtype = ' %s (on %s ) ' % ( args [ ' text ' ] , irc . get_full_network_name ( ) )
2017-08-31 23:19:30 +02:00
def _handle_operup_func ( irc , remoteirc , user ) :
2015-09-20 20:25:45 +02:00
log . debug ( ' ( %s ) relay.handle_opertype: setting OPERTYPE of %s / %s to %s ' ,
2017-08-31 23:19:30 +02:00
irc . name , user , remoteirc . name , newtype )
2015-09-15 02:23:56 +02:00
remoteirc . users [ user ] . opertype = newtype
2017-08-31 23:19:30 +02:00
iterate_all_present ( irc , numeric , _handle_operup_func )
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 ' ]
2017-08-18 21:39:47 +02:00
if not get_relay ( irc , 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
2016-12-10 02:57:02 +01:00
claim_passed = check_claim ( irc , channel , numeric )
2017-09-03 06:35:03 +02:00
current_chandata = irc . channels . get ( 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 :
2017-07-01 06:40:05 +02:00
oldmodes = set ( chandata . get_prefix_modes ( user ) )
2016-08-28 04:09:02 +02:00
except KeyError :
# User was never in channel. Treat their mode list as empty.
oldmodes = set ( )
2017-09-03 06:35:03 +02:00
newmodes = set ( )
if current_chandata is not None :
newmodes = set ( current_chandata . get_prefix_modes ( user ) )
2016-08-28 04:09:02 +02:00
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 )
2017-08-11 22:15:03 +02:00
# Special case for U-lined servers: allow them to join with ops,
# but don't forward this mode change on.
if modechar and not _is_uline ( irc , numeric ) :
2016-08-28 04:09:02 +02:00
modes . append ( ( ' - %s ' % modechar , user ) )
if modes :
2017-06-30 08:01:39 +02:00
log . debug ( ' ( %s ) relay.handle_join: reverting modes on BURST: %s ' , irc . name , irc . join_modes ( modes ) )
2017-06-25 11:07:24 +02:00
irc . mode ( irc . sid , channel , modes )
2016-08-28 04:09:02 +02:00
2016-12-10 02:57:02 +01:00
relay_joins ( irc , channel , users , ts , burst = False )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_join , ' JOIN ' )
2016-09-24 21:13:33 +02:00
utils . add_hook ( handle_join , ' PYLINK_SERVICE_JOIN ' )
2015-09-15 02:23:56 +02:00
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.
2017-06-30 08:10:53 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] from thread %r in function %r ' , irc . name , irc . name ,
threading . current_thread ( ) . name , inspect . currentframe ( ) . f_code . co_name )
2017-08-31 23:19:30 +02:00
2017-05-05 06:16:25 +02:00
if spawnlocks [ irc . name ] . acquire ( timeout = TCONDITION_TIMEOUT ) :
2017-08-31 23:19:30 +02:00
def _handle_quit_func ( irc , remoteirc , user ) :
2016-05-01 23:21:50 +02:00
try : # Try to quit the client. If this fails because they're missing, bail.
2017-06-25 11:07:24 +02:00
remoteirc . quit ( user , args [ ' text ' ] )
2016-05-01 23:21:50 +02:00
except LookupError :
pass
2017-08-31 23:19:30 +02:00
iterate_all_present ( irc , numeric , _handle_quit_func )
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 ]
2016-12-10 02:57:02 +01:00
initialize_all ( 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 ) :
2017-08-31 23:19:30 +02:00
newnick = args [ ' newnick ' ]
def _handle_nick_func ( irc , remoteirc , user ) :
remote_newnick = normalize_nick ( remoteirc , irc . name , newnick , uid = user )
if remoteirc . users [ user ] . nick != remote_newnick :
remoteirc . nick ( user , remote_newnick )
iterate_all_present ( irc , numeric , _handle_nick_func )
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.
2017-06-30 08:01:39 +02:00
if not irc . has_cap ( ' can-spawn-clients ' ) :
2017-08-18 21:39:47 +02:00
for channel in [ c for c in channels if get_relay ( irc , c ) ] :
2017-11-05 10:18:42 +01:00
for user in irc . channels [ channel ] . users . copy ( ) :
2017-07-11 07:38:25 +02:00
if ( not irc . is_internal_client ( user ) ) and ( not is_relay_client ( irc , user ) ) :
2017-06-30 08:01:39 +02:00
irc . call_hooks ( [ 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 ' } ] )
2017-06-25 11:07:24 +02:00
irc . join ( irc . pseudoclient . uid , channel )
2016-07-24 04:03:07 +02:00
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 :
2017-08-31 23:19:30 +02:00
def _handle_part_loop ( irc , remoteirc , user ) :
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( irc , remoteirc , channel )
2015-09-15 02:23:56 +02:00
if remotechan is None :
2017-08-31 23:19:30 +02:00
return
2017-06-25 11:07:24 +02:00
remoteirc . part ( user , remotechan , text )
2017-08-31 23:19:30 +02:00
2015-09-15 02:23:56 +02:00
if not remoteirc . users [ user ] . channels :
2017-06-25 11:07:24 +02:00
remoteirc . quit ( user , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , numeric ) ] [ remoteirc . name ]
2017-08-31 23:19:30 +02:00
iterate_all_present ( irc , numeric , _handle_part_loop )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_part , ' PART ' )
2018-02-19 07:24:16 +01:00
def _get_lowest_prefix ( prefixes ) :
2018-02-19 06:49:58 +01:00
if not prefixes :
return ' '
2018-02-19 07:24:16 +01:00
for prefix in ' vhoayq ' :
2018-02-19 06:49:58 +01:00
if prefix in prefixes :
return prefix
else :
2018-02-19 07:24:16 +01:00
log . warning ( ' relay._get_lowest_prefix: unknown prefixes string %r ' , prefixes )
2018-02-19 06:49:58 +01:00
return ' '
2015-09-26 19:10:54 +02:00
def handle_messages ( irc , numeric , command , args ) :
2018-01-09 05:54:49 +01:00
command = command . upper ( )
notice = ' NOTICE ' in command or command . startswith ( ' WALL ' )
2015-09-15 02:23:56 +02:00
target = args [ ' target ' ]
text = args [ ' text ' ]
2017-06-30 08:01:39 +02:00
if irc . is_internal_client ( numeric ) and irc . is_internal_client ( target ) :
2015-11-27 07:51:19 +01:00
# Drop attempted PMs between internal clients (this shouldn't happen,
# but whatever).
return
2017-01-30 05:21:45 +01:00
elif ( numeric in irc . servers ) and ( not notice ) :
2017-05-16 06:32:41 +02:00
log . debug ( ' ( %s ) relay.handle_messages: dropping PM from server %s to %s ' ,
irc . name , numeric , target )
2017-01-30 05:21:45 +01:00
return
2016-02-21 04:29:52 +01:00
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , target )
2015-09-15 02:23:56 +02:00
remoteusers = relayusers [ ( irc . name , numeric ) ]
2016-02-21 04:29:52 +01:00
2018-02-19 06:49:58 +01:00
avail_prefixes = { v : k for k , v in irc . prefixmodes . items ( ) }
prefixes = [ ]
# Split up @#channel prefixes and the like into their prefixes and target components
while target and target [ 0 ] in avail_prefixes :
prefixes . append ( avail_prefixes [ target [ 0 ] ] )
target = target [ 1 : ]
log . debug ( ' ( %s ) relay.handle_messages: splitting target %r into prefixes= %r , target= %r ' ,
irc . name , args [ ' target ' ] , prefixes , target )
2016-02-21 04:29:52 +01:00
2017-08-29 05:14:14 +02:00
if irc . is_channel ( target ) :
2018-02-19 06:49:58 +01:00
def _handle_messages_loop ( irc , remoteirc , numeric , command , args , notice ,
target , text , msgprefixes ) :
2016-12-10 02:57:02 +01:00
real_target = get_remote_channel ( 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.
2017-07-11 08:09:00 +02:00
if ( not real_target ) or ( not irc . connected . is_set ( ) ) :
return
2016-02-21 04:29:52 +01:00
2018-02-19 06:49:58 +01:00
orig_msgprefixes = msgprefixes
# Filter @#channel prefixes by what's available on the target network
msgprefixes = list ( filter ( lambda p : p in remoteirc . prefixmodes , msgprefixes ) )
log . debug ( " ( %s ) relay: filtering message prefixes for %s %s from %s to %s " ,
irc . name , remoteirc . name , real_target , orig_msgprefixes ,
msgprefixes )
# If we originally had message prefixes but ended with none,
# assume that we don't have a place to forward the message and drop it.
# One exception though is that %#channel implies @#channel.
if orig_msgprefixes and not msgprefixes :
if ' h ' in orig_msgprefixes :
msgprefixes . append ( ' o ' )
else :
log . debug ( " ( %s ) relay: dropping message for %s %s , orig_prefixes= %r since "
" prefixes were empty after filtering. " , irc . name ,
remoteirc . name , real_target , orig_msgprefixes )
return
# This bit of filtering exists because some IRCds let you do /msg ~@#channel
2018-02-19 07:24:16 +01:00
# and such, despite its redundancy. (This is equivalent to @#channel AFAIK)
lowest_msgprefix = _get_lowest_prefix ( msgprefixes )
lowest_msgprefix = remoteirc . prefixmodes . get ( lowest_msgprefix , ' ' )
real_target = lowest_msgprefix + real_target
2018-02-19 06:49:58 +01:00
2017-01-30 07:18:05 +01:00
user = get_remote_user ( irc , remoteirc , numeric , spawn_if_missing = False )
2016-06-25 20:28:51 +02:00
if not user :
2017-02-25 04:16:01 +01:00
if not ( irc . serverdata . get ( ' relay_weird_senders ' ,
conf . conf . get ( ' relay ' , { } ) . get ( ' accept_weird_senders ' , True ) ) ) :
log . debug ( " ( %s ) Dropping message for %s from user-less sender %s " , irc . name ,
real_target , numeric )
2017-07-11 08:09:00 +02:00
return
2016-06-25 20:28:51 +02:00
# No relay clone exists for the sender; route the message through our
2017-01-30 05:21:45 +01:00
# main client (or SID for notices).
2017-03-24 07:53:48 +01:00
# Skip "from:" formatting for servers; it's messy with longer hostnames.
# Also skip this formatting for servicebot relaying.
2017-06-30 08:01:39 +02:00
if numeric not in irc . servers and not irc . get_service_bot ( numeric ) :
displayedname = irc . get_friendly_name ( numeric )
2017-01-30 05:21:45 +01:00
real_text = ' < %s / %s > %s ' % ( displayedname , irc . name , text )
2016-05-01 23:38:51 +02:00
else :
2017-01-30 05:21:45 +01:00
real_text = text
2016-06-25 20:28:51 +02:00
2017-01-30 05:21:45 +01:00
# XXX: perhaps consider routing messages from the server where
# possible - most IRCds except TS6 (charybdis, ratbox, hybrid)
# allow this.
2016-06-25 20:28:51 +02:00
try :
2017-09-24 05:40:54 +02:00
user = get_relay_server_sid ( remoteirc , irc , spawn_if_missing = False ) \
2017-01-30 05:21:45 +01:00
if notice else remoteirc . pseudoclient . uid
if not user :
2017-07-11 08:09:00 +02:00
return
2016-06-25 20:28:51 +02:00
except AttributeError :
# Remote main client hasn't spawned yet. Drop the message.
2017-07-11 08:09:00 +02:00
return
2016-07-02 03:54:07 +02:00
else :
if remoteirc . pseudoclient . uid not in remoteirc . users :
# Remote UID is ghosted, drop message.
2017-07-11 08:09:00 +02:00
return
2016-07-02 03:54:07 +02:00
2016-06-25 20:28:51 +02:00
else :
real_text = text
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
2017-12-21 10:41:01 +01:00
if real_target . startswith ( tuple ( irc . prefixmodes . values ( ) ) ) and not \
remoteirc . has_cap ( ' has-statusmsg ' ) :
log . debug ( " ( %s ) Not sending message destined to %s / %s because "
" the remote does not support STATUSMSG. " , irc . name ,
remoteirc . name , real_target )
return
2016-07-02 03:54:07 +02:00
try :
if notice :
2017-06-25 11:07:24 +02:00
remoteirc . notice ( user , real_target , real_text )
2016-07-02 03:54:07 +02:00
else :
2017-06-25 11:07:24 +02:00
remoteirc . message ( user , real_target , real_text )
2016-07-02 03:54:07 +02:00
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 )
2017-07-11 08:09:00 +02:00
return
2018-02-19 06:49:58 +01:00
iterate_all ( irc , _handle_messages_loop ,
extra_args = ( numeric , command , args , notice , target , text , prefixes ) )
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
2016-12-10 02:57:02 +01:00
origuser = get_orig_user ( irc , target )
2016-02-21 04:29:52 +01:00
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 ( ) :
2016-11-19 07:52:08 +01:00
irc . msg ( numeric , ' You must be in a common channel '
2015-09-15 02:23:56 +02:00
' 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
2017-06-30 08:01:39 +02:00
if ( not remoteirc . has_cap ( ' can-spawn-clients ' ) ) and not conf . conf . get ( ' relay ' , { } ) . get ( ' allow_clientbot_pms ' ) :
2016-11-19 07:52:08 +01:00
irc . msg ( numeric , ' Private messages to users connected via Clientbot have '
2016-08-31 23:07:32 +02:00
' been administratively disabled. ' , notice = True )
2016-07-24 06:09:25 +02:00
return
2017-01-30 07:18:05 +01:00
user = get_remote_user ( irc , remoteirc , numeric , spawn_if_missing = False )
2016-02-21 04:29:52 +01:00
2016-07-02 03:54:07 +02:00
try :
if notice :
2017-06-25 11:07:24 +02:00
remoteirc . notice ( user , real_target , text )
2016-07-02 03:54:07 +02:00
else :
2017-06-25 11:07:24 +02:00
remoteirc . message ( user , real_target , text )
2016-07-02 03:54:07 +02:00
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 ' ) :
2017-09-03 06:17:54 +02:00
utils . add_hook ( handle_messages , cmd , priority = 500 )
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
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , 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.
2017-06-30 08:01:39 +02:00
if ( not irc . has_cap ( ' can-spawn-clients ' ) ) and irc . pseudoclient and target == irc . pseudoclient . uid :
2016-07-23 20:20:52 +02:00
for user in irc . channels [ channel ] . users :
2017-07-11 07:38:25 +02:00
if ( not irc . is_internal_client ( user ) ) and ( not is_relay_client ( irc , user ) ) :
2017-06-30 08:01:39 +02:00
reason = " Clientbot kicked by %s (Reason: %s ) " % ( irc . get_friendly_name ( source ) , text )
irc . call_hooks ( [ 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.
2017-06-30 08:01:39 +02:00
if relay is None or irc . get_service_bot ( target ) :
2015-09-15 02:23:56 +02:00
return
2016-05-15 01:24:26 +02:00
2016-12-10 02:57:02 +01:00
origuser = get_orig_user ( irc , target )
2017-07-11 08:09:00 +02:00
def _handle_kick_loop ( irc , remoteirc , source , command , args ) :
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( irc , remoteirc , channel )
2017-07-11 08:09:00 +02:00
name = remoteirc . name
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 )
2017-07-11 08:09:00 +02:00
2015-09-15 02:23:56 +02:00
if remotechan is None :
2017-07-11 08:09:00 +02:00
return
2017-01-30 07:18:05 +01:00
real_kicker = get_remote_user ( irc , remoteirc , kicker , spawn_if_missing = 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 )
2017-07-11 08:09:00 +02:00
2017-07-11 07:38:25 +02:00
if not is_relay_client ( 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 )
2017-07-11 08:09:00 +02:00
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.
2017-01-30 07:18:05 +01:00
real_target = get_remote_user ( irc , remoteirc , target , spawn_if_missing = 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 )
2016-12-10 02:57:02 +01:00
real_target = get_orig_user ( irc , target , targetirc = remoteirc )
2017-07-11 08:09:00 +02:00
2015-09-15 02:23:56 +02:00
if not real_target :
2017-07-11 08:09:00 +02:00
return
2015-09-15 02:23:56 +02:00
# 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 )
2017-06-25 11:07:24 +02:00
remoteirc . 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.
2017-09-24 05:40:54 +02:00
rsid = get_relay_server_sid ( 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 )
2017-07-11 08:09:00 +02:00
2017-06-30 08:01:39 +02:00
if not irc . has_cap ( ' can-spawn-clients ' ) :
2016-07-23 21:25:52 +02:00
# 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
2018-03-03 06:37:24 +01:00
text = " ( %s ) %s " % ( kname , args [ ' text ' ] )
2016-07-23 21:25:52 +02:00
except AttributeError :
2018-03-03 06:37:24 +01:00
text = " (<unknown kicker>) %s " % args [ ' text ' ]
2017-07-11 08:09:00 +02:00
2017-09-24 05:40:54 +02:00
rsid = rsid or remoteirc . sid # Fall back to the main PyLink SID if get_relay_server_sid() fails
2017-06-25 11:07:24 +02:00
remoteirc . 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 ]
2017-06-25 11:07:24 +02:00
remoteirc . quit ( real_target , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
2018-02-11 00:12:40 +01:00
if is_relay_client ( irc , target ) and not check_claim ( irc , channel , kicker ) :
2017-12-14 21:15:19 +01:00
homenet , real_target = get_orig_user ( irc , target )
homeirc = world . networkobjects . get ( homenet )
homenick = homeirc . users [ real_target ] . nick if homeirc else ' <ghost user> '
homechan = get_remote_channel ( irc , homeirc , channel )
log . debug ( ' ( %s ) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s ' , irc . name , kicker , real_target )
# Home network is not in the channel's claim AND the kicker is not
# 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 = get_prefix_modes ( homeirc , irc , homechan , real_target )
# Join the kicked client back with its respective modes.
irc . sjoin ( irc . sid , channel , [ ( modes , target ) ] )
if kicker in irc . users :
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from %s / %s to %s / %s on %s . ' ,
irc . name , args [ ' text ' ] , irc . users [ source ] . nick , irc . name ,
homenick , homenet , channel )
irc . msg ( kicker , " This channel is claimed; your kick to "
" %s has been blocked because you are not "
" (half)opped. " % channel , notice = True )
else :
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from server %s to %s / %s on %s . ' ,
irc . name , args [ ' text ' ] , irc . servers [ source ] . name ,
homenick , homenet , channel )
return
2017-07-11 08:09:00 +02:00
iterate_all ( irc , _handle_kick_loop , extra_args = ( source , command , args ) )
2015-09-15 02:23:56 +02:00
if origuser and not irc . users [ target ] . channels :
del relayusers [ origuser ] [ irc . name ]
2017-06-25 11:07:24 +02:00
irc . 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 :
2017-08-31 23:19:30 +02:00
def _handle_chgclient_loop ( irc , remoteirc , user ) :
2015-09-15 02:23:56 +02:00
try :
2015-11-22 08:37:19 +01:00
if field == ' HOST ' :
2016-12-10 02:57:02 +01:00
newtext = normalize_host ( remoteirc , text )
2016-08-22 03:06:53 +02:00
else : # Don't overwrite the original text variable on every iteration.
newtext = text
2017-07-01 06:30:20 +02:00
remoteirc . update_client ( 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 ,
2017-08-31 23:19:30 +02:00
user , remoteirc . name , target , irc . name )
return
iterate_all_present ( irc , target , _handle_chgclient_loop )
2015-09-15 02:23:56 +02:00
for c in ( ' CHGHOST ' , ' CHGNAME ' , ' CHGIDENT ' ) :
utils . add_hook ( handle_chgclient , c )
2015-07-20 07:43:26 +02:00
2017-08-11 22:15:03 +02:00
def _is_uline ( irc , client ) :
return irc . get_friendly_name ( irc . get_server ( client ) ) in irc . serverdata . get ( ' ulines ' , [ ] )
2015-07-15 04:39:49 +02:00
def handle_mode ( irc , numeric , command , args ) :
2017-12-14 21:15:19 +01:00
target = args [ ' target ' ]
modes = args [ ' modes ' ]
2017-08-12 04:22:14 +02:00
2017-12-14 21:42:43 +01:00
def _handle_mode_loop ( irc , remoteirc , numeric , command , target , modes ) :
2017-08-29 05:14:14 +02:00
if irc . is_channel ( target ) :
2017-12-14 21:15:19 +01:00
remotechan = get_remote_channel ( irc , remoteirc , target )
if not remotechan :
2017-07-11 08:09:00 +02:00
return
2017-12-14 21:15:19 +01:00
supported_modes = get_supported_cmodes ( irc , remoteirc , target , modes )
# Check if the sender is a user with a relay client; otherwise relay the mode
# from the corresponding server.
remotesender = get_remote_user ( irc , remoteirc , numeric , spawn_if_missing = False ) or \
get_relay_server_sid ( remoteirc , irc ) or remoteirc . sid
if not remoteirc . has_cap ( ' can-spawn-clients ' ) :
friendly_modes = [ ]
for modepair in modes :
modechar = modepair [ 0 ] [ - 1 ]
if modechar in irc . prefixmodes :
orig_user = get_orig_user ( irc , modepair [ 1 ] )
if orig_user and orig_user [ 0 ] == remoteirc . name :
# Don't display prefix mode changes for someone on the target clientbot
# link; this will either be relayed via modesync or ignored.
continue
# Convert UIDs to nicks when relaying this to clientbot.
modepair = ( modepair [ 0 ] , irc . get_friendly_name ( modepair [ 1 ] ) )
elif modechar in irc . cmodes [ ' *A ' ] and irc . is_hostmask ( modepair [ 1 ] ) and \
conf . conf . get ( ' relay ' , { } ) . get ( ' clientbot_modesync ' , ' none ' ) . lower ( ) != ' none ' :
# Don't show bans if the ban is a simple n!u@h and modesync is enabled
continue
friendly_modes . append ( modepair )
if friendly_modes :
# Call hooks, this is used for clientbot relay.
remoteirc . call_hooks ( [ remotesender , ' RELAY_RAW_MODE ' , { ' channel ' : remotechan , ' modes ' : friendly_modes } ] )
if supported_modes :
remoteirc . mode ( remotesender , remotechan , supported_modes )
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 ' )
2016-12-10 02:57:02 +01:00
modes = get_supported_umodes ( 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
2017-01-30 07:18:05 +01:00
remoteuser = get_remote_user ( irc , remoteirc , target , spawn_if_missing = False )
2016-02-28 03:36:20 +01:00
2015-09-02 07:13:29 +02:00
if remoteuser and modes :
2017-06-25 11:07:24 +02:00
remoteirc . mode ( remoteuser , remoteuser , modes )
2017-12-14 21:15:19 +01:00
2017-12-14 21:42:43 +01:00
reversed_modes = [ ]
2017-12-14 21:15:19 +01:00
if irc . is_channel ( target ) :
# Use the old state of the channel to check for CLAIM access.
oldchan = args . get ( ' channeldata ' )
2017-12-14 21:42:43 +01:00
# Block modedelta modes from being unset by leaf networks
relay_entry = get_relay ( irc , target )
if not relay_entry :
modedelta_modes = [ ]
else :
modedelta_modes = db [ relay_entry ] . get ( ' modedelta ' , [ ] )
modedelta_modes = list ( filter ( None , [ irc . cmodes . get ( named_modepair [ 0 ] )
for named_modepair in modedelta_modes ] ) )
2017-12-14 21:15:19 +01:00
if not check_claim ( irc , target , numeric , chanobj = oldchan ) :
# Mode change blocked by CLAIM.
reversed_modes = irc . reverse_modes ( target , modes , oldobj = oldchan )
if _is_uline ( irc , numeric ) :
# Special hack for "U-lined" servers - ignore changes to SIMPLE modes and
# attempts to op u-lined clients (trying to change status for others
# SHOULD be reverted).
# This is for compatibility with Anope's DEFCON for the most part, as well as
# silly people who try to register a channel multiple times via relay.
reversed_modes = [ modepair for modepair in reversed_modes if
# Mode is a prefix mode but target isn't ulined, revert
( ( modepair [ 0 ] [ - 1 ] in irc . prefixmodes and not
_is_uline ( irc , modepair [ 1 ] ) )
# Tried to set a list mode, revert
or modepair [ 0 ] [ - 1 ] in irc . cmodes [ ' *A ' ] )
]
2017-12-14 21:42:43 +01:00
modes . clear ( ) # Clear the mode list so nothing is relayed below
for modepair in modes . copy ( ) :
log . debug ( ' ( %s ) relay.handle_mode: checking if modepair %s is in %s ' ,
irc . name , str ( modepair ) , str ( modedelta_modes ) )
2017-12-14 22:07:00 +01:00
modechar = modepair [ 0 ] [ - 1 ]
if modechar in modedelta_modes :
if modechar in irc . cmodes [ ' *A ' ] or modechar in irc . prefixmodes :
# Don't enforce invalid modes.
log . debug ( ' ( %s ) relay.handle_mode: Not enforcing invalid modedelta mode %s on %s (list or prefix mode) ' ,
irc . name , str ( modepair ) , target )
continue
2017-12-14 22:26:43 +01:00
elif not irc . has_cap ( ' can-spawn-clients ' ) :
log . debug ( ' ( %s ) relay.handle_mode: Not enforcing modedelta modes on bot-only network ' ,
irc . name )
continue
2017-12-14 21:42:43 +01:00
modes . remove ( modepair )
if relay_entry [ 0 ] != irc . name :
# On leaf nets, enforce the modedelta.
reversed_modes + = irc . reverse_modes ( target , [ modepair ] , oldobj = oldchan )
log . debug ( ' ( %s ) relay.handle_mode: Reverting change of modedelta mode %s on %s ' ,
irc . name , str ( modepair ) , target )
else :
# On the home net, just don't propagate the mode change.
log . debug ( ' ( %s ) relay.handle_mode: Not propagating change of modedelta mode %s on %s ' ,
irc . name , str ( modepair ) , target )
2017-12-14 21:15:19 +01:00
2017-12-14 21:42:43 +01:00
if reversed_modes :
log . debug ( ' ( %s ) relay.handle_mode: Reversing mode changes of %r with %r . ' ,
irc . name , args [ ' modes ' ] , reversed_modes )
irc . mode ( irc . sid , target , reversed_modes )
2017-12-14 21:15:19 +01:00
2017-12-14 21:42:43 +01:00
if modes :
iterate_all ( irc , _handle_mode_loop , extra_args = ( numeric , command , target , 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 ' ]
2017-07-11 08:09:00 +02:00
2016-12-10 02:57:02 +01:00
if check_claim ( irc , channel , numeric ) :
2017-07-11 08:09:00 +02:00
def _handle_topic_loop ( irc , remoteirc , numeric , command , args ) :
channel = args [ ' channel ' ]
oldtopic = args . get ( ' oldtopic ' )
topic = args [ ' text ' ]
2015-09-13 23:23:27 +02:00
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( irc , remoteirc , channel )
2017-07-11 08:09:00 +02:00
2015-09-13 23:23:27 +02:00
# Don't send if the remote topic is the same as ours.
2017-08-25 11:29:10 +02:00
if remotechan is None or remotechan not in remoteirc . channels or \
topic == remoteirc . channels [ remotechan ] . topic :
2017-07-11 08:09:00 +02:00
return
2015-09-13 23:23:27 +02:00
# This might originate from a server too.
2017-01-30 07:18:05 +01:00
remoteuser = get_remote_user ( irc , remoteirc , numeric , spawn_if_missing = False )
2015-09-13 23:23:27 +02:00
if remoteuser :
2017-06-25 11:07:24 +02:00
remoteirc . topic ( remoteuser , remotechan , topic )
2015-09-13 23:23:27 +02:00
else :
2017-09-24 05:40:54 +02:00
rsid = get_relay_server_sid ( remoteirc , irc )
2017-07-01 06:30:20 +02:00
remoteirc . topic_burst ( rsid , remotechan , topic )
2017-07-11 08:09:00 +02:00
iterate_all ( irc , _handle_topic_loop , extra_args = ( numeric , command , args ) )
2015-09-14 02:58:39 +02:00
elif oldtopic : # Topic change blocked by claim.
2017-07-01 06:30:20 +02:00
irc . topic_burst ( 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 :
2016-12-10 02:57:02 +01:00
realuser = get_orig_user ( irc , target )
2016-03-08 03:10:36 +01:00
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 :
2016-12-10 02:57:02 +01:00
localchan = get_remote_channel ( remoteirc , irc , remotechan )
2016-03-26 00:39:06 +01:00
if localchan :
2016-12-10 02:57:02 +01:00
modes = get_prefix_modes ( remoteirc , irc , remotechan , realuser [ 1 ] )
2016-03-26 00:39:06 +01:00
log . debug ( ' ( %s ) relay.handle_kill: userpair: %s , %s ' , irc . name , modes , realuser )
2016-12-10 02:57:02 +01:00
client = get_remote_user ( remoteirc , irc , realuser [ 1 ] , times_tagged = 1 )
2017-09-24 05:40:54 +02:00
irc . sjoin ( get_relay_server_sid ( irc , remoteirc ) , localchan , [ ( modes , client ) ] )
2016-03-26 00:39:06 +01:00
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 ) :
2017-08-31 23:19:30 +02:00
iterate_all_present ( irc , numeric ,
lambda irc , remoteirc , user :
remoteirc . 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 ' ]
2017-07-11 07:38:25 +02:00
if is_relay_client ( irc , target ) :
2016-12-10 02:57:02 +01:00
remotenet , remoteuser = get_orig_user ( irc , target )
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ remotenet ]
2016-12-10 02:57:02 +01:00
remotechan = get_remote_channel ( irc , remoteirc , channel )
2017-01-30 07:18:05 +01:00
remotesource = get_remote_user ( irc , remoteirc , source , spawn_if_missing = False )
2015-09-15 02:23:56 +02:00
if remotesource is None :
2016-11-19 07:52:08 +01:00
irc . msg ( source , ' You must be in a common channel '
2015-09-15 02:23:56 +02:00
' with %s to invite them to channels. ' % \
irc . users [ target ] . nick ,
notice = True )
elif remotechan is None :
2016-11-19 07:52:08 +01:00
irc . msg ( source , ' You cannot invite someone to a '
2015-09-15 02:23:56 +02:00
' channel not on their network! ' ,
notice = True )
else :
2017-06-25 11:07:24 +02:00
remoteirc . 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 :
2016-12-10 02:57:02 +01:00
initialize_all ( irc )
2015-09-29 03:15:56 +02:00
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 .
"""
2017-08-31 23:19:30 +02:00
iterate_all_present ( irc , numeric ,
lambda irc , remoteirc , user :
remoteirc . call_hooks ( [ user , ' PYLINK_RELAY_SERVICES_LOGIN ' , args ] ) )
2016-07-08 06:43:25 +02:00
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). """
2017-02-25 06:15:40 +01:00
2015-10-11 00:34:57 +02:00
# Quit all of our users' representations on other nets, and remove
# them from our relay clients index.
2017-06-30 08:10:53 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks[ %s ] from thread %r in function %r ' , irc . name , irc . name ,
threading . current_thread ( ) . name , inspect . currentframe ( ) . f_code . co_name )
2017-05-05 06:16:25 +02:00
if spawnlocks [ irc . name ] . acquire ( timeout = TCONDITION_TIMEOUT ) :
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.
2017-06-30 08:10:53 +02:00
log . debug ( ' ( %s ) Grabbing spawnlocks_servers[ %s ] from thread %r in function %r ' , irc . name , irc . name ,
threading . current_thread ( ) . name , inspect . currentframe ( ) . f_code . co_name )
2017-05-05 06:16:25 +02:00
if spawnlocks_servers [ irc . name ] . acquire ( timeout = TCONDITION_TIMEOUT ) :
2017-07-11 08:09:00 +02:00
def _handle_disconnect_loop ( irc , remoteirc ) :
name = remoteirc . name
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 :
2017-07-11 08:09:00 +02:00
return
2016-03-31 03:33:44 +02:00
else :
2017-07-11 08:09:00 +02:00
remoteirc . proto . squit ( remoteirc . sid , rsid , text = ' Relay network lost connection. ' )
2016-03-31 03:33:44 +02:00
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
2017-07-11 08:09:00 +02:00
iterate_all ( irc , _handle_disconnect_loop )
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
2017-02-25 06:15:40 +01:00
# Announce the disconnects to every leaf channel where the disconnected network is the owner
announcement = conf . conf . get ( ' relay ' , { } ) . get ( ' disconnect_announcement ' )
2017-02-26 07:06:43 +01:00
log . debug ( ' ( %s ) relay: last connection successful: %s ' , irc . name , args . get ( ' was_successful ' ) )
2017-02-25 06:42:58 +01:00
if announcement and args . get ( ' was_successful ' ) :
2017-05-05 03:50:42 +02:00
for chanpair , entrydata in db . items ( ) :
log . debug ( ' ( %s ) relay: Looking up %s ' , irc . name , chanpair )
if chanpair [ 0 ] == irc . name :
for leaf in entrydata [ ' links ' ] :
log . debug ( ' ( %s ) relay: Announcing disconnect to %s %s ' , irc . name ,
leaf [ 0 ] , leaf [ 1 ] )
remoteirc = world . networkobjects . get ( leaf [ 0 ] )
if remoteirc and remoteirc . connected . is_set ( ) :
text = string . Template ( announcement ) . safe_substitute (
{ ' homenetwork ' : irc . name , ' homechannel ' : chanpair [ 1 ] ,
' network ' : remoteirc . name , ' channel ' : leaf [ 1 ] } )
remoteirc . msg ( leaf [ 1 ] , text , loopback = False )
2017-02-25 06:15:40 +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 .
"""
2016-12-10 02:57:02 +01:00
remotenet , remoteuser = get_orig_user ( irc , target )
2016-07-11 08:48:10 +02:00
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.
2016-12-10 02:57:02 +01:00
newnick = normalize_nick ( irc , remotenet , nick , times_tagged = 1 )
2016-10-21 05:13:17 +02:00
log . debug ( ' ( %s ) relay.nick_collide: Fixing nick of relay client %r ( %s ) to %s ' ,
irc . name , target , nick , newnick )
2017-06-25 11:07:24 +02:00
irc . nick ( target , newnick )
2016-07-11 08:48:10 +02:00
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
2017-07-11 07:38:25 +02:00
if is_relay_client ( 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 ' ]
2017-07-11 07:38:25 +02:00
if is_relay_client ( irc , target ) :
2016-07-12 08:23:26 +02:00
nick_collide ( irc , target )
utils . add_hook ( handle_svsnick , " SVSNICK " )
2017-12-21 09:19:09 +01:00
def handle_knock ( irc , source , command , args ) :
def _handle_knock_loop ( irc , remoteirc , source , command , args ) :
channel = args [ ' channel ' ]
remotechan = get_remote_channel ( irc , remoteirc , channel )
# TS6 does not use reasons with knock, so this is not always available.
text = args [ ' text ' ] or ' No reason available '
if remotechan is None or remotechan not in remoteirc . channels :
return
remoteuser = get_remote_user ( irc , remoteirc , source , spawn_if_missing = False )
use_fallback = False
if remoteuser :
try :
remoteirc . knock ( remoteuser , remotechan , text )
except NotImplementedError :
use_fallback = True
else :
use_fallback = True
# Fallback to a simple notice directed to ops
if use_fallback :
2017-12-21 10:56:04 +01:00
nick = irc . get_friendly_name ( source )
log . debug ( ' ( %s ) relay: using fallback KNOCK routine for %s on %s / %s ' ,
irc . name , nick , remoteirc . name , remotechan )
prefix = ' % ' if ' h ' in remoteirc . prefixmodes else ' @ '
remoteirc . notice ( remoteirc . pseudoclient . uid , prefix + remotechan ,
" Knock from %s @ %s (*not* invitable from this network): %s " %
( nick , irc . name , text ) )
2017-12-21 09:19:09 +01:00
iterate_all ( irc , _handle_knock_loop , extra_args = ( source , command , args ) )
utils . add_hook ( handle_knock , ' KNOCK ' )
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 :
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 0 ] )
2015-07-11 05:26:46 +02:00
except IndexError :
2016-11-19 07:52:08 +01:00
irc . error ( " Not enough arguments. Needs 1: channel. " )
2015-07-12 22:09:35 +02:00
return
2017-08-29 05:14:14 +02:00
if not irc . is_channel ( channel ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2017-06-30 08:01:39 +02:00
if not irc . has_cap ( ' can-host-relay ' ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' Clientbot networks cannot be used to host a relay. ' )
2016-08-04 19:55:00 +02:00
return
2017-08-25 11:29:10 +02:00
if channel not in irc . channels or source not in irc . channels [ channel ] . users :
2016-11-19 07:52:08 +01:00
irc . 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
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.create ' ] )
2015-11-29 06:09:16 +01:00
# Check to see whether the channel requested is already part of a different
# relay.
2017-08-18 21:39:47 +02:00
localentry = get_relay ( irc , channel )
2015-09-18 04:24:38 +02:00
if localentry :
2016-11-19 07:52:08 +01:00
irc . 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
2017-06-30 08:01:39 +02:00
creator = irc . get_hostmask ( 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.
2017-05-05 03:50:42 +02:00
db [ ( irc . name , channel ) ] = { ' claim ' : [ irc . name ] , ' links ' : set ( ) ,
' 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 )
2016-12-10 02:57:02 +01:00
initialize_channel ( 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
2016-12-10 02:57:02 +01:00
def stop_relay ( entry ) :
2016-12-10 02:34:51 +01:00
""" Internal function to deinitialize a relay link and its leaves. """
network , channel = entry
# Iterate over all the channel links and deinitialize them.
2017-05-05 03:50:42 +02:00
for link in db [ entry ] [ ' links ' ] :
remove_channel ( world . networkobjects . get ( link [ 0 ] ) , link [ 1 ] )
remove_channel ( world . networkobjects . get ( network ) , channel )
2016-12-10 02:34:51 +01:00
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.
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( 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.
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 0 ] )
2016-03-05 18:31:59 +01:00
network = irc . name
except IndexError :
2016-11-19 07:52:08 +01:00
irc . error ( " Not enough arguments. Needs 1-2: channel, network (optional). " )
2016-03-05 18:31:59 +01:00
return
2017-08-29 05:14:14 +02:00
if not irc . is_channel ( channel ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-11-08 06:18:20 +01:00
# Check for different permissions based on whether we're destroying a local channel or
# a remote one.
2016-03-05 18:31:59 +01:00
if network == irc . name :
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.destroy ' ] )
2016-03-05 18:31:59 +01:00
else :
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.destroy.remote ' ] )
2016-03-05 18:31:59 +01:00
entry = ( network , channel )
2017-05-05 03:50:42 +02:00
if entry in db :
stop_relay ( entry )
del db [ entry ]
2016-03-05 18:31:59 +01:00
2017-05-05 03:50:42 +02:00
log . info ( ' ( %s ) relay: Channel %s destroyed by %s . ' , irc . name ,
2017-06-30 08:01:39 +02:00
channel , irc . get_hostmask ( source ) )
2017-05-05 03:50:42 +02:00
irc . reply ( ' Done. ' )
else :
irc . error ( " No such channel %r exists. If you ' re trying to delink a channel from "
" another network, use the DESTROY command. " % channel )
return
2016-07-01 03:05:27 +02:00
destroy = utils . add_cmd ( destroy , featured = True )
2015-07-11 05:26:46 +02:00
2016-12-10 02:34:51 +01:00
@utils.add_cmd
def purge ( irc , source , args ) :
""" <network>
Destroys all links relating to the target network . """
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.purge ' ] )
2016-12-10 02:34:51 +01:00
try :
network = args [ 0 ]
except IndexError :
irc . error ( " Not enough arguments. Needs 1: network. " )
return
count = 0
2017-05-05 03:50:42 +02:00
for entry in db . copy ( ) :
# Entry was owned by the target network; remove it
if entry [ 0 ] == network :
count + = 1
stop_relay ( entry )
del db [ entry ]
else :
# Drop leaf channels involving the target network
for link in db [ entry ] [ ' links ' ] . copy ( ) :
if link [ 0 ] == network :
count + = 1
remove_channel ( world . networkobjects . get ( network ) , link [ 1 ] )
db [ entry ] [ ' links ' ] . remove ( link )
2016-12-10 02:34:51 +01:00
irc . reply ( " Done. Purged %s entries involving the network %s . " % ( count , network ) )
2017-02-22 06:52:01 +01:00
link_parser = utils . IRCParser ( )
link_parser . add_argument ( ' remotenet ' )
link_parser . add_argument ( ' channel ' )
link_parser . add_argument ( ' localchannel ' , nargs = ' ? ' )
link_parser . add_argument ( " -f " , " --force " , action = ' store_true ' )
2015-07-13 02:59:09 +02:00
def link ( irc , source , args ) :
2017-02-22 06:52:01 +01:00
""" <remotenet> <channel> [<local channel>] [-f/--force]
2015-07-13 02:59:09 +02:00
2016-07-01 03:05:27 +02:00
Links the specified channel on \x02remotenet \x02 over PyLink Relay as \x02local channel \x02 .
2017-02-22 06:52:01 +01:00
If \x02local channel \x02 is not specified , it defaults to the same name as \x02channel \x02 .
2016-06-19 21:13:56 +02:00
2017-02-22 06:52:01 +01:00
If the - - force option is given , this command will bypass checks for TS and whether the target
network is alive , and link the channel anyways . """
args = link_parser . parse_args ( args )
2017-03-10 05:36:54 +01:00
# Normalize channel case
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args . channel )
localchan = irc . to_lower ( args . localchannel or args . channel )
2017-02-22 06:52:01 +01:00
remotenet = args . remotenet
2016-06-19 21:13:56 +02:00
2017-03-10 05:36:54 +01:00
for c in ( channel , localchan ) :
2017-08-29 05:14:14 +02:00
if not irc . is_channel ( c ) :
2016-11-19 07:52:08 +01:00
irc . 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 :
2016-11-19 07:52:08 +01:00
irc . error ( ' Cannot link two channels on the same network. ' )
2016-06-19 21:13:56 +02:00
return
2017-08-25 11:29:10 +02:00
if localchan not in irc . channels or 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 :
2017-06-25 11:07:24 +02:00
irc . 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
2017-07-11 11:23:13 +02:00
elif not irc . channels [ localchan ] . is_op_plus ( source ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' You must be opped in %r to complete this operation. ' % localchan )
2016-08-04 19:47:06 +02:00
return
else :
2016-11-19 07:52:08 +01:00
irc . error ( ' You must be in %r to complete this operation. ' % localchan )
2016-08-04 19:47:06 +02:00
return
2016-06-19 21:13:56 +02:00
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.link ' ] )
2016-06-19 21:13:56 +02:00
2015-08-29 18:39:33 +02:00
if remotenet not in world . networkobjects :
2016-11-19 07:52:08 +01:00
irc . error ( ' No network named %r exists. ' % remotenet )
2015-07-13 02:59:09 +02:00
return
2017-08-18 21:39:47 +02:00
localentry = get_relay ( irc , localchan )
2016-06-19 21:13:56 +02:00
2015-07-14 07:54:51 +02:00
if localentry :
2016-11-19 07:52:08 +01:00
irc . 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 :
2017-05-05 03:50:42 +02:00
entry = db [ ( remotenet , channel ) ]
2015-07-13 02:59:09 +02:00
except KeyError :
2017-02-22 06:52:01 +01:00
irc . error ( ' No such relay %r exists. ' % args . 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-11-19 07:52:08 +01:00
irc . 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 :
2016-11-19 07:52:08 +01:00
irc . error ( " Remote channel ' %s %s ' is already linked here "
2017-02-22 06:52:01 +01:00
" as %r . " % ( remotenet , args . channel , link [ 1 ] ) )
2015-07-14 07:54:51 +02:00
return
2016-08-04 22:04:32 +02:00
2017-02-22 06:52:01 +01:00
if args . force :
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.link.force ' ] )
2017-02-22 06:52:01 +01:00
log . info ( " ( %s ) relay: Forcing link %s %s -> %s %s " , irc . name , irc . name , localchan , remotenet ,
args . channel )
else :
2017-02-22 06:58:32 +01:00
if not world . networkobjects [ remotenet ] . connected . is_set ( ) :
log . debug ( ' ( %s ) relay: Blocking link request %s %s -> %s %s because the target '
' network is down ' , irc . name , irc . name , localchan , remotenet , args . channel )
irc . error ( " The target network %s is not connected; refusing to link (you may be "
" able to override this with the --force option). " % remotenet )
return
2017-02-22 06:52:01 +01:00
our_ts = irc . channels [ localchan ] . ts
2017-08-25 11:29:10 +02:00
if channel not in world . networkobjects [ remotenet ] . channels :
irc . error ( " Unknown target channel %r . " % channel )
return
2017-03-28 17:08:39 +02:00
their_ts = world . networkobjects [ remotenet ] . channels [ channel ] . ts
2017-06-30 08:01:39 +02:00
if ( our_ts < their_ts ) and irc . has_cap ( ' has-ts ' ) :
2017-02-22 06:52:01 +01:00
log . debug ( ' ( %s ) relay: Blocking link request %s %s -> %s %s due to bad TS ( %s < %s ) ' , irc . name ,
irc . name , localchan , remotenet , args . channel , our_ts , their_ts )
irc . error ( " The channel creation date (TS) on %s ( %s ) is lower than the target "
" channel ' s ( %s ); refusing to link. You should clear the local channel %s first "
2017-02-22 06:58:32 +01:00
" before linking, or use a different local channel (you may be able to "
" override this with the --force option). " % ( localchan , our_ts , their_ts , localchan ) )
2017-02-22 06:52:01 +01:00
return
2016-08-04 22:04:32 +02:00
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 ,
2017-06-30 08:01:39 +02:00
localchan , remotenet , args . channel , irc . get_hostmask ( source ) )
2016-12-10 02:57:02 +01:00
initialize_channel ( 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 :
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 0 ] )
2015-07-13 02:59:09 +02:00
except IndexError :
2016-11-19 07:52:08 +01:00
irc . 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-11-08 06:18:20 +01:00
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.delink ' ] )
2016-11-08 06:18:20 +01:00
2017-08-29 05:14:14 +02:00
if not irc . is_channel ( channel ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' Invalid channel %r . ' % channel )
2015-07-13 02:59:09 +02:00
return
2017-08-18 21:39:47 +02:00
entry = get_relay ( irc , 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 :
2016-11-19 07:52:08 +01:00
irc . 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 :
2017-05-05 03:50:42 +02:00
for link in db [ entry ] [ ' links ' ] . copy ( ) :
if link [ 0 ] == remotenet :
remove_channel ( world . networkobjects . get ( remotenet ) , link [ 1 ] )
db [ entry ] [ ' links ' ] . remove ( link )
2015-07-13 02:59:09 +02:00
else :
2016-12-10 02:57:02 +01:00
remove_channel ( irc , channel )
2017-05-05 03:50:42 +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 ,
2017-06-30 08:01:39 +02:00
channel , entry [ 0 ] , entry [ 1 ] , irc . get_hostmask ( source ) )
2015-07-13 02:59:09 +02:00
else :
2016-11-19 07:52:08 +01:00
irc . 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
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.linked ' ] )
2016-12-25 09:31:38 +01:00
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
2017-05-05 03:50:42 +02:00
for k , v in sorted ( db . items ( ) ) :
# 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
2016-05-22 07:55:06 +02:00
2017-05-05 03:50:42 +02:00
# Bold each network/channel name pair
s = ' \x02 %s %s \x02 ' % k
remoteirc = world . networkobjects . get ( k [ 0 ] )
channel = k [ 1 ] # Get the channel name from the network/channel pair
if remoteirc and channel in remoteirc . channels :
c = remoteirc . channels [ channel ]
if ( ' s ' , None ) in c . modes or ( ' p ' , None ) in c . modes :
# Only show secret channels to opers or those in the channel, and tag them as
# [secret].
localchan = get_remote_channel ( remoteirc , irc , channel )
2017-06-30 08:01:39 +02:00
if irc . is_oper ( source ) or ( localchan and source in irc . channels [ localchan ] . users ) :
2017-05-05 03:50:42 +02:00
s + = ' \x02 [secret] \x02 '
else :
continue
2015-11-29 06:09:16 +01:00
2017-05-05 03:50:42 +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
2017-05-05 03:50:42 +02:00
else : # Unless it's empty; then, well... just say no relays yet.
s + = ' (no relays yet) '
2015-11-29 06:09:16 +01:00
2017-05-05 03:50:42 +02:00
irc . reply ( s , private = True )
2015-08-12 13:18:20 +02:00
2017-06-30 08:01:39 +02:00
if irc . is_oper ( source ) :
2017-05-05 03:50:42 +02:00
s = ' '
2016-01-10 05:30:54 +01:00
2017-05-05 03:50:42 +02:00
# If the caller is an oper, we can show the hostmasks of people
# that created all the available channels (Janus does this too!!)
creator = v . get ( ' creator ' )
if creator :
# But only if the value actually exists (old DBs will have it
# missing).
s + = ' by \x02 %s \x02 ' % creator
2016-01-10 05:30:54 +01:00
2017-05-05 03:50:42 +02:00
# Ditto for creation date
ts = v . get ( ' ts ' )
if ts :
s + = ' on %s ' % time . ctime ( ts )
2016-01-10 05:30:54 +01:00
2017-05-05 03:50:42 +02:00
if s : # Indent to make the list look nicer
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 .
2016-12-06 08:33:42 +01:00
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 . """
2016-11-19 07:52:08 +01:00
missingargs = " Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY). "
2016-11-08 06:18:20 +01:00
2015-08-26 05:18:14 +02:00
try :
cmd = args [ 0 ] . lower ( )
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 1 ] )
2015-08-26 05:18:14 +02:00
except IndexError :
2016-11-19 07:52:08 +01:00
irc . error ( missingargs )
2015-08-26 05:18:14 +02:00
return
2017-08-29 05:14:14 +02:00
if not irc . is_channel ( channel ) :
2016-11-19 07:52:08 +01:00
irc . error ( ' Invalid channel %r . ' % channel )
2015-08-26 05:18:14 +02:00
return
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , channel )
2015-08-26 05:18:14 +02:00
if not relay :
2016-11-19 07:52:08 +01:00
irc . error ( ' No such relay %r exists. ' % channel )
2015-08-26 05:18:14 +02:00
return
2017-05-05 03:50:42 +02:00
if cmd == ' list ' :
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.linkacl.view ' ] )
2017-05-05 03:50:42 +02:00
s = ' Blocked networks for \x02 %s \x02 : \x02 %s \x02 ' % ( channel , ' , ' . join ( db [ relay ] [ ' blocked_nets ' ] ) or ' (empty) ' )
irc . reply ( s )
return
2015-08-26 05:18:14 +02:00
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.linkacl ' ] )
2017-05-05 03:50:42 +02:00
try :
remotenet = args [ 2 ]
except IndexError :
irc . error ( missingargs )
return
if cmd == ' deny ' :
db [ relay ] [ ' blocked_nets ' ] . add ( remotenet )
irc . reply ( ' Done. ' )
elif cmd == ' allow ' :
2015-08-26 05:18:14 +02:00
try :
2017-05-05 03:50:42 +02:00
db [ relay ] [ ' blocked_nets ' ] . remove ( remotenet )
except KeyError :
irc . error ( ' Network %r is not on the blacklist for %r . ' % ( remotenet , channel ) )
2016-12-10 02:43:50 +01:00
else :
2017-05-05 03:50:42 +02:00
irc . reply ( ' Done. ' )
else :
irc . 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
2017-06-30 08:01:39 +02:00
u = irc . nick_to_uid ( 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 :
2016-12-10 02:57:02 +01:00
userpair = get_orig_user ( 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 :
2017-08-18 21:39:47 +02:00
relay = get_relay ( irc , ch )
2015-08-30 04:29:05 +02:00
if relay :
relaychannels . append ( ' ' . join ( relay ) )
2017-06-30 08:01:39 +02:00
if relaychannels and ( irc . is_oper ( 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
2016-11-12 19:43:55 +01:00
@utils.add_cmd
def showchan ( irc , source , args ) :
""" <user>
Shows relay data about the given channel . This supplements the ' showchan ' command in the ' commands ' plugin , which provides more general information . """
try :
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 0 ] )
2016-11-12 19:43:55 +01:00
except IndexError :
return
if channel not in irc . channels :
return
f = lambda s : irc . reply ( s , private = True )
c = irc . channels [ channel ]
# Only show verbose info if caller is oper or is in the target channel.
2017-06-30 08:01:39 +02:00
verbose = source in c . users or irc . is_oper ( source )
2016-11-12 19:43:55 +01:00
secret = ( ' s ' , None ) in c . modes
if secret and not verbose :
# Hide secret channels from normal users.
return
else :
2017-08-18 21:39:47 +02:00
relayentry = get_relay ( irc , channel )
2016-11-12 19:43:55 +01:00
if relayentry :
relays = [ ' \x02 %s \x02 ' % ' ' . join ( relayentry ) ]
relays + = [ ' ' . join ( link ) for link in db [ relayentry ] [ ' links ' ] ]
f ( ' \x02 Relayed channels: \x02 %s ' % ( ' ' . join ( relays ) ) )
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 . """
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.savedb ' ] )
2016-11-10 07:47:22 +01:00
datastore . save ( )
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>]
2017-02-06 06:42:15 +01:00
Sets the CLAIM for a channel to a case - sensitive list of networks . If no list of networks is
given , this shows which networks have a claim over the channel . A single hyphen ( - ) can also be
given as a list of networks to disable CLAIM from the channel entirely .
CLAIM is a way of enforcing network ownership for channels , similar to Janus . Unless a
channel ' s CLAIM list is empty, only networks on its CLAIM list (plus the creating network) are
allowed to override kicks , mode changes , and topic changes - attempts from other networks are
either blocked or reverted . " override " in this case refers to any KICK , MODE , or TOPIC change
from a sender that is not halfop or above in the channel ( this affects servers and services
as well ) .
"""
2015-09-17 05:59:32 +02:00
try :
2017-06-30 08:01:39 +02:00
channel = irc . to_lower ( args [ 0 ] )
2015-09-17 05:59:32 +02:00
except IndexError :
2016-11-19 07:52:08 +01:00
irc . error ( " Not enough arguments. Needs 1-2: channel, list of networks (optional). " )
2015-09-17 05:59:32 +02:00
return
2017-08-02 16:24:23 +02:00
permissions . check_permissions ( irc , source , [ ' relay.claim ' ] )
2016-11-08 06:18:20 +01:00
2016-12-10 02:57:02 +01:00
# We override get_relay() here to limit the search to the current network.
2015-09-17 05:59:32 +02:00
relay = ( irc . name , channel )
2017-05-05 03:50:42 +02:00
if relay not in db :
irc . error ( ' No relay %r exists on this network (this command must be run on the '
' network this channel was created on). ' % channel )
return
claimed = db [ relay ] [ " claim " ]
try :
nets = args [ 1 ] . strip ( )
except IndexError : # No networks given.
irc . reply ( ' Channel \x02 %s \x02 is claimed by: %s ' %
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )
else :
2017-07-03 07:06:50 +02:00
if nets == ' - ' or not nets :
claimed = set ( )
else :
claimed = set ( nets . split ( ' , ' ) )
2017-05-05 03:50:42 +02:00
db [ relay ] [ " claim " ] = claimed
irc . reply ( ' CLAIM for channel \x02 %s \x02 set to: %s ' %
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )
2017-04-01 07:52:40 +02:00
@utils.add_cmd
def modedelta ( irc , source , args ) :
""" <channel> [<named modes>]
Sets the relay mode delta for the given channel : a list of named mode pairs to apply on leaf
2017-12-14 20:56:41 +01:00
channels , but not the host network .
This may be helpful in fighting spam if leaf networks
2017-04-01 07:52:40 +02:00
don ' t police it as well as your own (e.g. you can set +R with this).
Mode names are defined using PyLink named modes , and not IRC mode characters : you can find a
2017-12-14 22:07:00 +01:00
list of channel named modes and the characters they map to on different IRCds at :
2017-12-14 20:56:41 +01:00
https : / / raw . githack . com / GLolol / PyLink / devel / docs / modelists / channel - modes . html
2017-04-01 07:52:40 +02:00
Examples of setting modes :
2017-12-14 20:56:41 +01:00
2017-04-01 07:52:40 +02:00
modedelta #channel regonly
2017-12-14 20:56:41 +01:00
2017-04-01 07:52:40 +02:00
modedelta #channel regonly inviteonly
2017-12-14 20:56:41 +01:00
2017-04-01 07:52:40 +02:00
modedelta #channel key,supersecret sslonly
2017-12-14 20:56:41 +01:00
2017-04-01 07:52:40 +02:00
modedelta #channel -
If no modes are given , this shows the mode delta for the channel .
2017-12-14 20:56:41 +01:00
2017-04-01 07:52:40 +02:00
A single hyphen ( - ) can also be given as a list of modes to disable the mode delta
and remove any mode deltas from relay leaves .
2017-12-14 22:07:00 +01:00
Note : only simple and single - arg modes ( e . g . + f , + l ) are supported ; list modes and
prefix modes such as bans and op are NOT .
2017-04-01 07:52:40 +02:00
"""
try :
2017-12-14 20:56:41 +01:00
channel = irc . to_lower ( args [ 0 ] )
2017-04-01 07:52:40 +02:00
except IndexError :
irc . error ( " Not enough arguments. Needs 1-2: channel, list of modes (optional). " )
return
2017-12-14 20:56:41 +01:00
permissions . check_permissions ( irc , source , [ ' relay.modedelta ' ] )
2017-04-01 07:52:40 +02:00
# We override get_relay() here to limit the search to the current network.
relay = ( irc . name , channel )
2017-12-14 20:56:41 +01:00
if relay not in db :
irc . error ( ' No relay %r exists on this network (this command must be run on the '
' network this channel was created on). ' % channel )
return
2017-04-01 07:52:40 +02:00
2017-12-14 20:56:41 +01:00
target_modes = [ ]
old_modes = [ ]
if ' - ' in args [ 1 : ] : # - given to clear the list
try :
2017-12-14 22:07:00 +01:00
# Keep track of the old modedelta modes, and unset them when applying the new ones
2017-12-14 20:56:41 +01:00
old_modes = db [ relay ] [ ' modedelta ' ]
del db [ relay ] [ ' modedelta ' ]
except KeyError :
irc . error ( ' No mode delta exists for %r . ' % channel )
return
2017-04-01 07:52:40 +02:00
else :
2017-12-14 20:56:41 +01:00
irc . reply ( ' Cleared the mode delta for %r . ' % channel )
else :
modes = [ ]
2017-12-14 22:40:47 +01:00
for modepair in args [ 1 : ] :
2017-12-14 20:56:41 +01:00
# Construct mode pairs given the initial query.
m = modepair . split ( ' , ' , 1 )
if len ( m ) == 1 :
m . append ( None )
2017-12-14 22:07:00 +01:00
# Sanity check: you shouldn't be allowed to lock things like op or redirects
# because one misconfiguration can cause serious desyncs.
2017-12-18 22:23:16 +01:00
if m [ 0 ] not in WHITELISTED_CMODES :
2017-12-14 22:40:47 +01:00
irc . error ( ' Setting mode %r is not supported for modedelta (case sensitive). ' % m [ 0 ] )
2017-12-14 22:07:00 +01:00
return
2017-12-14 20:56:41 +01:00
modes . append ( m )
if modes :
old_modes = db [ relay ] . get ( ' modedelta ' , [ ] )
db [ relay ] [ ' modedelta ' ] = target_modes = modes
log . debug ( ' channel: %s ' , str ( channel ) )
irc . reply ( ' Set the mode delta for \x02 %s \x02 to: %s ' % ( channel , modes ) )
else : # No modes given, so show the list.
irc . reply ( ' Mode delta for channel \x02 %s \x02 is set to: %s ' %
( channel , db [ relay ] . get ( ' modedelta ' ) or ' \x1D (none) \x1D ' ) )
# Add to target_modes all former modedelta modes that don't have a positive equivalent
# Note: We only check for (modechar, modedata) and not for (+modechar, modedata) here
# internally, but the actual filtering below checks for both?
modedelta_diff = [ ( ' - %s ' % modepair [ 0 ] , modepair [ 1 ] ) for modepair in old_modes if
modepair not in target_modes ]
target_modes + = modedelta_diff
for chanpair in db [ relay ] [ ' links ' ] :
remotenet , remotechan = chanpair
remoteirc = world . networkobjects . get ( remotenet )
if not remoteirc :
continue
2017-04-01 07:52:40 +02:00
2017-12-14 20:56:41 +01:00
remote_modes = [ ]
# For each leaf channel, unset the old mode delta and set the new one
# if applicable.
log . debug ( ' ( %s ) modedelta target modes for %s / %s : %s ' , irc . name , remotenet , remotechan , target_modes )
for modepair in target_modes :
modeprefix = modepair [ 0 ] [ 0 ]
if modeprefix not in ' +- ' : # Assume + if no prefix was given.
modeprefix = ' + '
modename = modepair [ 0 ] . lstrip ( ' +- ' )
2017-12-14 22:07:00 +01:00
2017-12-14 20:56:41 +01:00
mchar = remoteirc . cmodes . get ( modename )
if mchar :
2017-12-14 22:07:00 +01:00
if mchar in remoteirc . cmodes [ ' *A ' ] or mchar in remoteirc . prefixmodes :
log . warning ( ' ( %s ) Refusing to set modedelta mode %r on %s because it is a list or prefix mode ' ,
irc . name , mchar , remotechan )
continue
2017-12-14 22:26:43 +01:00
elif not remoteirc . has_cap ( ' can-spawn-clients ' ) :
log . debug ( ' ( %s ) relay.handle_mode: Not enforcing modedelta modes on bot-only network %s ' ,
irc . name , remoteirc . name )
continue
2017-12-14 20:56:41 +01:00
remote_modes . append ( ( ' %s %s ' % ( modeprefix , mchar ) , modepair [ 1 ] ) )
if remote_modes :
log . debug ( ' ( %s ) Sending modedelta modes %s to %s / %s ' , irc . name , remote_modes , remotenet , remotechan )
remoteirc . mode ( remoteirc . pseudoclient . uid , remotechan , remote_modes )