2015-07-11 05:26:46 +02:00
# relay.py: PyLink Relay plugin
import sys
import os
sys . path . append ( os . path . dirname ( os . path . dirname ( os . path . abspath ( __file__ ) ) ) )
2015-12-29 20:13:50 +01:00
2015-07-11 05:26:46 +02:00
import pickle
2015-12-29 20:13:50 +01:00
import time
2015-07-11 05:26:46 +02:00
import threading
2015-07-13 01:59:49 +02:00
import string
2015-07-13 08:28:54 +02:00
from collections import defaultdict
2015-07-11 05:26:46 +02:00
import utils
from log import log
2015-08-29 18:39:33 +02:00
import world
2015-07-11 05:26:46 +02:00
2015-12-07 02:13:47 +01:00
### GLOBAL (statekeeping) VARIABLES
2015-07-13 08:28:54 +02:00
relayusers = defaultdict ( dict )
2015-09-12 21:06:58 +02:00
relayservers = defaultdict ( dict )
spawnlocks = defaultdict ( threading . RLock )
2015-09-21 01:56:24 +02:00
spawnlocks_servers = defaultdict ( threading . RLock )
2016-03-26 00:39:06 +01:00
2016-02-28 03:13:26 +01:00
exportdb_timer = None
2015-12-27 02:06:28 +01:00
2015-12-07 02:13:47 +01:00
dbname = utils . getDatabaseName ( ' pylinkrelay ' )
2015-09-15 02:23:56 +02:00
### INTERNAL FUNCTIONS
def initializeAll ( irc ) :
""" Initializes all relay channels for the given IRC object. """
2016-02-28 03:36:20 +01:00
# Wait for all IRC objects to be created first. This prevents
2015-10-11 00:21:38 +02:00
# relay servers from being spawned too early (before server authentication),
# which would break connections.
world . started . wait ( 2 )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
for chanpair , entrydata in db . items ( ) :
2016-02-28 03:36:20 +01:00
# Iterate over all the channels stored in our relay links DB.
2015-09-15 02:23:56 +02:00
network , channel = chanpair
2016-02-28 03:36:20 +01:00
# Initialize each relay channel on their home network, and on every linked one too.
2015-09-15 02:23:56 +02:00
initializeChannel ( irc , channel )
for link in entrydata [ ' links ' ] :
network , channel = link
initializeChannel ( irc , channel )
2015-09-27 20:56:09 +02:00
def main ( irc = None ) :
2015-09-15 02:23:56 +02:00
""" Main function, called during plugin loading at start. """
2015-12-27 02:06:28 +01:00
2016-02-28 03:13:26 +01:00
# Load the relay links database.
2015-09-15 02:23:56 +02:00
loadDB ( )
2015-12-27 02:06:28 +01:00
2016-02-28 03:13:26 +01:00
# Schedule periodic exports of the links database.
scheduleExport ( starting = True )
2015-12-27 02:06:28 +01:00
2015-09-27 20:56:09 +02:00
if irc is not None :
2016-02-28 03:13:26 +01:00
# irc is defined when the plugin is reloaded. Otherweise,
# it means that we've just started the server.
# Iterate over all known networks and initialize them.
2015-09-27 20:56:09 +02:00
for ircobj in world . networkobjects . values ( ) :
initializeAll ( ircobj )
def die ( sourceirc ) :
2015-12-27 02:06:28 +01:00
""" Deinitialize PyLink Relay by quitting all relay clients and saving the
relay DB . """
2016-02-28 03:13:26 +01:00
# For every connected network:
2015-09-27 20:56:09 +02:00
for irc in world . networkobjects . values ( ) :
2016-02-28 03:13:26 +01:00
# 1) Find all the relay clients and quit them.
2015-09-27 20:56:09 +02:00
for user in irc . users . copy ( ) :
if isRelayClient ( irc , user ) :
2016-01-17 01:51:54 +01:00
irc . proto . quit ( user , " Relay plugin unloaded. " )
2016-02-28 03:13:26 +01:00
# 2) SQUIT every relay subserver.
2015-09-27 20:56:09 +02:00
for server , sobj in irc . servers . copy ( ) . items ( ) :
if hasattr ( sobj , ' remote ' ) :
2016-01-17 01:53:06 +01:00
irc . proto . squit ( irc . sid , server , text = " Relay plugin unloaded. " )
2016-02-28 03:13:26 +01:00
# 3) 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-02-28 03:13:26 +01:00
# 4) Export the relay links database.
exportDB ( )
# 5) Kill the scheduling for any other exports.
global exportdb_timer
if exportdb_timer :
log . debug ( " Relay: cancelling exportDB timer thread %s due to die() " , threading . get_ident ( ) )
exportdb_timer . cancel ( )
2015-12-27 02:06:28 +01:00
2015-08-23 05:51:50 +02:00
def normalizeNick ( irc , netname , nick , separator = None , uid = ' ' ) :
2015-09-15 02:23:56 +02:00
""" Creates a normalized nickname for the given nick suitable for
introduction to a remote network ( as a relay client ) . """
2015-07-20 23:36:47 +02:00
separator = separator or irc . serverdata . get ( ' separator ' ) or " / "
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.normalizeNick: using %r as separator. ' , irc . name , separator )
2015-07-12 23:02:17 +02:00
orig_nick = nick
2015-09-06 03:00:57 +02:00
protoname = irc . protoname
2015-07-12 23:02:17 +02:00
maxnicklen = irc . maxnicklen
2015-09-01 18:56:28 +02:00
if ' / ' not in separator or not protoname . startswith ( ( ' insp ' , ' unreal ' ) ) :
2015-08-23 05:51:50 +02:00
# Charybdis doesn't allow / in usernames, and will SQUIT with
# a protocol violation if it sees one.
2015-07-12 23:02:17 +02:00
separator = separator . replace ( ' / ' , ' | ' )
nick = nick . replace ( ' / ' , ' | ' )
2015-07-13 01:59:49 +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!
2015-07-13 01:59:49 +02:00
nick = ' _ ' + nick
2015-07-12 23:02:17 +02:00
tagnicks = True
suffix = separator + netname
nick = nick [ : maxnicklen ]
2015-08-23 05:51:50 +02:00
# Maximum allowed length of a nickname, minus the obligatory /network tag.
2015-07-12 23:02:17 +02:00
allowedlength = maxnicklen - len ( suffix )
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 ]
nick + = suffix
2015-08-23 05:51:50 +02:00
# The nick we want exists? Darn, create another one then.
# 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.
# However, if the user is changing from, say, a long, cut-off nick to another long,
# cut-off nick, we don't need to check for duplicates and tag the nick twice.
# somecutoffnick/net would otherwise be erroneous NICK'ed to somecutoffnic//net,
# even though there would be no collision because the old and new nicks are from
# the same client.
2016-01-01 02:28:47 +01:00
while irc . nickToUid ( nick ) and irc . nickToUid ( nick ) != uid :
2015-07-12 23:02:17 +02:00
new_sep = separator + separator [ - 1 ]
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.normalizeNick: nick %r is in use; using %r as new_sep. ' , irc . name , nick , new_sep )
2015-07-13 08:28:54 +02:00
nick = normalizeNick ( irc , netname , orig_nick , separator = new_sep )
2015-07-12 23:02:17 +02:00
finalLength = len ( nick )
assert finalLength < = maxnicklen , " Normalized nick %r went over max " \
2015-09-10 05:53:04 +02:00
" nick length (got: %s , allowed: %s !) " % ( nick , finalLength , maxnicklen )
2015-07-12 23:02:17 +02:00
return nick
2015-11-22 08:37:19 +01:00
def normalizeHost ( irc , host ) :
""" Creates a normalized hostname for the given host suitable for
introduction to a remote network ( as a relay client ) . """
if irc . protoname == ' unreal ' :
# UnrealIRCd doesn't allow slashes in hostnames
host = host . replace ( ' / ' , ' . ' )
return host [ : 64 ] # Limited to 64 chars
2015-07-11 05:26:46 +02:00
def loadDB ( ) :
2015-09-15 02:23:56 +02:00
""" Loads the relay database, creating a new one if this fails. """
2015-07-11 05:26:46 +02:00
global db
try :
with open ( dbname , " rb " ) as f :
db = pickle . load ( f )
2016-04-02 03:31:53 +02:00
except ( ValueError , IOError , OSError ) :
log . info ( " Relay: failed to load links database %s "
" , creating a new one in memory... " , dbname )
2015-07-11 05:26:46 +02:00
db = { }
2016-02-28 03:13:26 +01:00
def exportDB ( ) :
""" Exports the relay database. """
log . debug ( " Relay: exporting links database to %s " , dbname )
with open ( dbname , ' wb ' ) as f :
pickle . dump ( db , f , protocol = 4 )
def scheduleExport ( starting = False ) :
"""
Schedules exporting of the relay database in a repeated loop .
"""
global exportdb_timer
if not starting :
2016-03-31 06:22:18 +02:00
# Export the database, unless this is being called the first
2016-02-28 03:13:26 +01:00
# thing after start (i.e. DB has just been loaded).
exportDB ( )
# TODO: possibly make delay between exports configurable
exportdb_timer = threading . Timer ( 30 , scheduleExport )
exportdb_timer . name = ' PyLink Relay exportDB Loop '
exportdb_timer . start ( )
2015-07-11 05:26:46 +02:00
2015-09-13 22:48:14 +02:00
def getPrefixModes ( irc , remoteirc , channel , user , mlist = None ) :
"""
Fetches all prefix modes for a user in a channel that are supported by the
remote IRC network given .
Optionally , an mlist argument can be given to look at an earlier state of
2016-02-28 03:36:20 +01:00
the channel , e . g . for checking the op status of a mode setter before their
2015-09-13 22:48:14 +02:00
modes are processed and added to the channel state .
"""
2015-07-14 06:46:05 +02:00
modes = ' '
2016-02-28 03:36:20 +01:00
2016-03-20 01:54:42 +01:00
if user in irc . channels [ channel ] . users :
# Iterate over the the prefix modes for relay supported by IRCd
for pmode in irc . channels [ channel ] . getPrefixModes ( user , prefixmodes = mlist ) :
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
2015-09-12 21:06:58 +02:00
def getRemoteSid ( irc , remoteirc ) :
2015-09-15 02:23:56 +02:00
""" Gets the remote server SID representing remoteirc on irc, spawning
2015-09-12 21:06:58 +02:00
it if it doesn ' t exist. " " "
2015-10-11 00:21:38 +02:00
# Don't spawn servers too early.
irc . connected . wait ( 2 )
2015-11-16 06:42:58 +01:00
2015-09-29 03:21:52 +02:00
try :
spawnservers = irc . conf [ ' relay ' ] [ ' spawn_servers ' ]
except KeyError :
spawnservers = True
if not spawnservers :
return irc . sid
2015-09-21 01:56:24 +02:00
with spawnlocks_servers [ irc . name ] :
2015-09-12 21:06:58 +02:00
try :
sid = relayservers [ irc . name ] [ remoteirc . name ]
except KeyError :
2015-09-19 07:11:27 +02:00
try :
2015-12-18 06:18:16 +01:00
# ENDBURST is delayed by 3 secs on supported IRCds to prevent
# triggering join-flood protection and the like.
2015-09-20 21:11:41 +02:00
sid = irc . proto . spawnServer ( ' %s .relay ' % remoteirc . name ,
desc = " PyLink Relay network - %s " %
( remoteirc . serverdata . get ( ' netname ' ) \
2015-12-18 06:18:16 +01:00
or remoteirc . name ) , endburst_delay = 3 )
2016-04-02 21:46:45 +02:00
except ValueError : # Network not initialized yet, or a server name conflict.
2015-09-19 07:11:27 +02:00
log . exception ( ' ( %s ) Failed to spawn server for %r : ' ,
irc . name , remoteirc . name )
2016-04-02 21:46:45 +02:00
# We will just bail here. Disconnect the bad network.
handle_disconnect ( irc , None , ' PYLINK_DISCONNECT_RELAY_FORCED ' , { } )
irc . disconnect ( )
raise
2015-09-27 20:56:09 +02:00
else :
irc . servers [ sid ] . remote = remoteirc . name
2015-09-12 21:06:58 +02:00
relayservers [ irc . name ] [ remoteirc . name ] = sid
return sid
2015-07-20 08:49:50 +02:00
def getRemoteUser ( irc , remoteirc , user , spawnIfMissing = True ) :
2015-09-15 02:23:56 +02:00
""" Gets the UID of the relay client for the given IRC network/user pair,
spawning one if it doesn ' t exist and spawnIfMissing is True. " " "
2015-07-18 01:55:44 +02:00
# If the user (stored here as {('netname', 'UID'):
# {'network1': 'UID1', 'network2': 'UID2'}}) exists, don't spawn it
2015-07-14 21:04:05 +02:00
# again!
2015-07-17 01:27:17 +02:00
try :
if user == irc . pseudoclient . uid :
return remoteirc . pseudoclient . uid
except AttributeError : # Network hasn't been initialized yet?
pass
2015-08-14 17:52:09 +02:00
with spawnlocks [ irc . name ] :
try :
u = relayusers [ ( irc . name , user ) ] [ remoteirc . name ]
except KeyError :
userobj = irc . users . get ( user )
if userobj is None or ( not spawnIfMissing ) or ( not remoteirc . connected . is_set ( ) ) :
# The query wasn't actually a valid user, or the network hasn't
# been connected yet... Oh well!
return
nick = normalizeNick ( remoteirc , irc . name , userobj . nick )
# Truncate idents at 10 characters, because TS6 won't like them otherwise!
ident = userobj . ident [ : 10 ]
2015-11-22 08:37:19 +01:00
# Normalize hostnames
host = normalizeHost ( remoteirc , userobj . host )
2015-08-14 17:52:09 +02:00
realname = userobj . realname
2015-11-22 22:08:31 +01:00
modes = set ( getSupportedUmodes ( irc , remoteirc , userobj . modes ) )
2015-08-31 23:52:56 +02:00
opertype = ' '
2015-08-31 23:23:42 +02:00
if ( ' o ' , None ) in userobj . modes :
if hasattr ( userobj , ' opertype ' ) :
# InspIRCd's special OPERTYPE command; this is mandatory
# and setting of umode +/-o will fail unless this
# is used instead. This also sets an oper type for
# the user, which is used in WHOIS, etc.
# If an opertype exists for the user, add " (remote)"
# for the relayed clone, so that it shows in whois.
# Janus does this too. :)
log . debug ( ' ( %s ) relay.getRemoteUser: setting OPERTYPE of client for %r to %s ' ,
irc . name , user , userobj . opertype )
2015-09-20 20:25:45 +02:00
opertype = userobj . opertype + ' (remote) '
2015-08-31 23:52:56 +02:00
else :
2015-09-20 20:25:45 +02:00
opertype = ' IRC Operator (remote) '
2015-08-31 23:23:42 +02:00
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc . umodes . get ( ' hideoper ' )
2015-09-19 19:17:25 +02:00
try :
use_hideoper = irc . conf [ ' relay ' ] [ ' hideoper ' ]
except KeyError :
use_hideoper = True
if hideoper_mode and use_hideoper :
2015-11-22 22:08:31 +01:00
modes . add ( ( hideoper_mode , None ) )
2015-09-12 21:06:58 +02:00
rsid = getRemoteSid ( remoteirc , irc )
2015-09-19 19:17:25 +02:00
try :
2016-04-18 21:29:15 +02:00
showRealIP = irc . conf [ ' relay ' ] [ ' show_ips ' ] and not \
irc . serverdata . get ( ' relay_no_ips ' ) and not \
remoteirc . serverdata . get ( ' relay_no_ips ' )
2015-09-19 19:17:25 +02:00
except KeyError :
showRealIP = False
if showRealIP :
ip = userobj . ip
realhost = userobj . realhost
else :
realhost = None
ip = ' 0.0.0.0 '
2015-09-29 03:21:52 +02:00
u = remoteirc . proto . spawnClient ( nick , ident = ident ,
2015-09-19 07:11:27 +02:00
host = host , realname = realname ,
modes = modes , ts = userobj . ts ,
2015-09-19 19:17:25 +02:00
opertype = opertype , server = rsid ,
ip = ip , realhost = realhost ) . uid
2015-08-16 04:18:04 +02:00
remoteirc . users [ u ] . remote = ( irc . name , user )
2015-08-31 23:52:56 +02:00
remoteirc . users [ u ] . opertype = opertype
2015-08-14 17:52:09 +02:00
away = userobj . away
if away :
2016-01-17 01:40:36 +01:00
remoteirc . proto . away ( u , away )
2015-08-14 17:52:09 +02:00
relayusers [ ( irc . name , user ) ] [ remoteirc . name ] = u
return u
2015-07-14 21:04:05 +02:00
2015-09-15 02:29:37 +02:00
def getOrigUser ( irc , user , targetirc = None ) :
2015-09-15 02:23:56 +02:00
"""
Given the UID of a relay client , returns a tuple of the home network name
and original UID of the user it was spawned for .
2015-07-22 08:47:06 +02:00
2015-09-15 02:23:56 +02:00
If targetirc is given , getRemoteUser ( ) is called to get the relay client
representing the original user on that target network . """
2015-07-15 04:39:49 +02:00
# First, iterate over everyone!
2015-08-16 04:18:04 +02:00
try :
remoteuser = irc . users [ user ] . remote
except ( AttributeError , KeyError ) :
remoteuser = None
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.getOrigUser: remoteuser set to %r (looking up %s / %s ). ' ,
2015-09-15 02:23:56 +02:00
irc . name , remoteuser , user , irc . name )
2015-07-22 08:47:06 +02:00
if remoteuser :
# If targetirc is given, we'll return simply the UID of the user on the
# target network, if it exists. Otherwise, we'll return a tuple
# with the home network name and the original user's UID.
2015-08-29 18:39:33 +02:00
sourceobj = world . networkobjects . get ( remoteuser [ 0 ] )
2015-07-22 08:47:06 +02:00
if targetirc and sourceobj :
if remoteuser [ 0 ] == targetirc . name :
# The user we found's home network happens to be the one being
# requested; just return the UID then.
return remoteuser [ 1 ]
# Otherwise, use getRemoteUser to find our UID.
2015-09-15 02:23:56 +02:00
res = getRemoteUser ( sourceobj , targetirc , remoteuser [ 1 ] ,
spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.getOrigUser: targetirc found, getting %r as '
2015-09-15 02:23:56 +02:00
' remoteuser for %r (looking up %s / %s ). ' , irc . name , res ,
remoteuser [ 1 ] , user , irc . name )
2015-07-22 08:47:06 +02:00
return res
else :
2015-07-15 04:39:49 +02:00
return remoteuser
2015-09-15 02:29:37 +02:00
def getRelay ( chanpair ) :
2015-09-17 05:59:08 +02:00
""" Finds the matching relay entry name for the given (network name, channel)
pair , if one exists . """
2015-07-13 08:28:54 +02:00
if chanpair in db : # This chanpair is a shared channel; others link to it
return chanpair
# This chanpair is linked *to* a remote channel
for name , dbentry in db . items ( ) :
if chanpair in dbentry [ ' links ' ] :
return name
2015-07-13 04:03:18 +02:00
2015-09-15 02:29:37 +02:00
def getRemoteChan ( irc , remoteirc , channel ) :
2015-09-15 02:23:56 +02:00
""" Returns the linked channel name for the given channel on remoteirc,
if one exists . """
2015-07-14 21:04:05 +02:00
query = ( irc . name , channel )
remotenetname = remoteirc . name
2015-09-15 02:29:37 +02:00
chanpair = getRelay ( query )
2015-07-14 04:46:24 +02:00
if chanpair is None :
return
if chanpair [ 0 ] == remotenetname :
return chanpair [ 1 ]
else :
for link in db [ chanpair ] [ ' links ' ] :
if link [ 0 ] == remotenetname :
return link [ 1 ]
2015-07-13 09:01:04 +02:00
def initializeChannel ( irc , channel ) :
2015-09-15 02:23:56 +02:00
""" Initializes a relay channel (merge local/remote users, set modes, etc.). """
2015-07-14 06:46:05 +02:00
# We're initializing a relay that already exists. This can be done at
# ENDBURST, or on the LINK command.
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel being called on %s ' , irc . name , channel )
log . debug ( ' ( %s ) relay.initializeChannel: relay pair found to be %s ' , irc . name , relay )
2015-07-14 21:04:05 +02:00
queued_users = [ ]
2015-07-14 06:46:05 +02:00
if relay :
all_links = db [ relay ] [ ' links ' ] . copy ( )
all_links . update ( ( relay , ) )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel: all_links: %s ' , irc . name , all_links )
2015-07-23 04:29:58 +02:00
# Iterate over all the remote channels linked in this relay.
2015-07-14 06:46:05 +02:00
for link in all_links :
2015-07-13 09:01:04 +02:00
remotenet , remotechan = link
2016-02-28 03:36:20 +01:00
if remotenet == irc . name : # If the network is us, skip.
2015-07-14 06:46:05 +02:00
continue
2015-08-29 18:39:33 +02:00
remoteirc = world . networkobjects . get ( remotenet )
2016-02-28 03:36:20 +01:00
2015-07-21 08:36:26 +02:00
if remoteirc is None :
2016-02-28 03:36:20 +01:00
# Remote network doesn't have an IRC object; e.g. it was removed
# from the config. Skip this.
2015-07-21 08:36:26 +02:00
continue
2015-07-14 06:46:05 +02:00
rc = remoteirc . channels [ remotechan ]
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
if not ( remoteirc . connected . is_set ( ) and getRemoteChan ( remoteirc , irc , remotechan ) ) :
2016-02-28 03:36:20 +01:00
continue # Remote network isn't connected.
2015-07-23 04:29:58 +02:00
# Join their (remote) users and set their modes.
2015-08-16 08:31:54 +02:00
relayJoins ( remoteirc , remotechan , rc . users , rc . ts )
2015-08-16 08:05:09 +02:00
topic = remoteirc . channels [ remotechan ] . topic
2016-02-28 03:36:20 +01:00
2015-07-20 22:18:04 +02:00
# Only update the topic if it's different from what we already have,
# and topic bursting is complete.
2015-08-16 08:05:09 +02:00
if remoteirc . channels [ remotechan ] . topicset and topic != irc . channels [ channel ] . topic :
2016-01-17 02:09:52 +01:00
irc . proto . topicBurst ( getRemoteSid ( irc , remoteirc ) , channel , topic )
2016-02-28 03:36:20 +01:00
2015-09-03 02:41:49 +02:00
# Send our users and channel modes to the other nets
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.initializeChannel: joining our ( %s ) users: %s ' , irc . name , remotenet , irc . channels [ channel ] . users )
2015-09-12 21:06:58 +02:00
relayJoins ( irc , channel , irc . channels [ channel ] . users , irc . channels [ channel ] . ts )
2016-04-18 20:52:01 +02:00
if irc . pseudoclient and irc . pseudoclient . uid not in irc . channels [ channel ] . users :
2016-01-17 01:36:45 +01:00
irc . proto . join ( irc . pseudoclient . uid , channel )
2015-07-14 21:04:05 +02:00
2015-09-15 02:23:56 +02:00
def removeChannel ( irc , channel ) :
""" Destroys a relay channel by parting all of its users. """
if irc is None :
2015-07-29 13:02:45 +02:00
return
2016-03-05 18:31:59 +01:00
2015-09-15 02:23:56 +02:00
if channel not in map ( str . lower , irc . serverdata [ ' channels ' ] ) :
2016-01-17 01:51:54 +01:00
irc . proto . part ( irc . pseudoclient . uid , channel , ' Channel delinked. ' )
2016-03-05 18:31:59 +01:00
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-09-15 02:23:56 +02:00
if relay :
for user in irc . channels [ channel ] . users . copy ( ) :
if not isRelayClient ( irc , user ) :
relayPart ( irc , channel , user )
# Don't ever part the main client from any of its autojoin channels.
2015-08-15 09:02:46 +02:00
else :
2015-09-15 02:23:56 +02:00
if user == irc . pseudoclient . uid and channel in \
irc . serverdata [ ' channels ' ] :
continue
2016-01-17 01:51:54 +01:00
irc . proto . part ( user , channel , ' Channel delinked. ' )
2015-09-15 02:23:56 +02:00
# Don't ever quit it either...
if user != irc . pseudoclient . uid and not irc . users [ user ] . channels :
2015-09-15 02:29:37 +02:00
remoteuser = getOrigUser ( irc , user )
2015-09-15 02:23:56 +02:00
del relayusers [ remoteuser ] [ irc . name ]
2016-01-17 01:51:54 +01:00
irc . proto . quit ( user , ' Left all shared channels. ' )
2015-07-15 04:39:49 +02:00
2015-09-13 22:48:14 +02:00
def checkClaim ( irc , channel , sender , chanobj = None ) :
2015-09-13 07:50:53 +02:00
"""
Checks whether the sender of a kick / mode change passes CLAIM checks for
a given channel . This returns True if any of the following criteria are met :
2015-09-17 05:59:08 +02:00
1 ) No relay exists for the channel in question .
2 ) The originating network is the one that created the relay .
3 ) The CLAIM list for the relay in question is empty .
4 ) The originating network is in the CLAIM list for the relay in question .
5 ) The sender is halfop or above in the channel .
6 ) The sender is a PyLink client / server ( checks are suppressed in this case ) .
2015-09-13 07:50:53 +02:00
"""
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-09-13 22:48:14 +02:00
try :
mlist = chanobj . prefixmodes
except AttributeError :
mlist = None
2016-03-20 01:54:42 +01:00
2015-09-13 22:48:14 +02:00
sender_modes = getPrefixModes ( irc , irc , channel , sender , mlist = mlist )
log . debug ( ' ( %s ) relay.checkClaim: sender modes ( %s / %s ) are %s (mlist= %s ) ' , irc . name ,
sender , channel , sender_modes , mlist )
2016-03-20 01:54:42 +01:00
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
2015-09-17 05:59:08 +02:00
return ( not relay ) or irc . name == relay [ 0 ] or not db [ relay ] [ ' claim ' ] or \
irc . name in db [ relay ] [ ' claim ' ] or \
2015-09-13 07:50:53 +02:00
any ( [ mode in sender_modes for mode in ( ' y ' , ' q ' , ' a ' , ' o ' , ' h ' ) ] ) \
2016-01-01 02:28:47 +01:00
or irc . isInternalClient ( sender ) or \
irc . isInternalServer ( sender )
2015-09-13 07:50:53 +02:00
2015-09-15 02:23:56 +02:00
def getSupportedUmodes ( irc , remoteirc , modes ) :
""" Given a list of user modes, filters out all of those not supported by the
remote network . """
supported_modes = [ ]
2016-02-28 03:36:20 +01:00
# Iterate over all mode pairs.
2015-09-15 02:23:56 +02:00
for modepair in modes :
try :
2016-02-28 03:36:20 +01:00
# Get the prefix and the actual mode character (the prefix being + or -, or
# whether we're setting or unsetting a mode)
2015-09-15 02:23:56 +02:00
prefix , modechar = modepair [ 0 ]
except ValueError :
2016-02-28 03:36:20 +01:00
# If the prefix is missing, assume we're adding a mode.
2015-09-15 02:23:56 +02:00
modechar = modepair [ 0 ]
prefix = ' + '
2016-02-28 03:36:20 +01:00
# Get the mode argument.
2015-09-15 02:23:56 +02:00
arg = modepair [ 1 ]
2016-02-28 03:36:20 +01:00
# Iterate over all supported user modes for the current network.
2015-09-15 02:23:56 +02:00
for name , m in irc . umodes . items ( ) :
supported_char = None
2016-02-28 03:36:20 +01:00
# Mode character matches one in our list, so set that named mode
# as the one we're trying to set. Then, look up that named mode
# in the supported modes list for the TARGET network, and set that
# mode character as the one we're setting, if it exists.
2015-09-15 02:23:56 +02:00
if modechar == m :
if name not in whitelisted_umodes :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.getSupportedUmodes: skipping mode ( %r , %r ) because "
2015-09-15 02:23:56 +02:00
" it isn ' t a whitelisted (safe) mode for relay. " ,
irc . name , modechar , arg )
break
supported_char = remoteirc . umodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if supported_char :
supported_modes . append ( ( prefix + supported_char , arg ) )
break
else :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.getSupportedUmodes: skipping mode ( %r , %r ) because "
2015-09-15 02:23:56 +02:00
" the remote network ( %s ) ' s IRCd ( %s ) doesn ' t support it. " ,
irc . name , modechar , arg , remoteirc . name ,
remoteirc . protoname )
return supported_modes
def isRelayClient ( irc , user ) :
""" Returns whether the given user is a relay client. """
try :
if irc . users [ user ] . remote :
# Is the .remote attribute set? If so, don't relay already
# relayed clients; that'll trigger an endless loop!
return True
except AttributeError : # Nope, it isn't.
pass
except KeyError : # The user doesn't exist?!?
return True
return False
### EVENT HANDLER INTERNALS
def relayJoins ( irc , channel , users , ts , burst = True ) :
2016-02-28 03:36:20 +01:00
"""
Relays one or more users ' joins from a channel to its relay links.
"""
2015-11-27 07:50:20 +01:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
queued_users = [ ]
if name == irc . name or not remoteirc . connected . is_set ( ) :
# Don't relay things to their source network...
2015-07-15 03:20:20 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-07-15 03:20:20 +02:00
if remotechan is None :
2016-02-28 03:36:20 +01:00
# If there is no link on the current network for the channel in question,
# just skip it
2015-07-15 03:20:20 +02:00
continue
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayJoins: got %r for users ' , irc . name , users )
2015-09-15 02:23:56 +02:00
for user in users . copy ( ) :
if isRelayClient ( irc , user ) :
2016-02-28 03:36:20 +01:00
# Don't clone relay clients; that'll cause bad infinite loops.
2015-09-15 02:23:56 +02:00
continue
2016-02-28 03:36:20 +01:00
assert user in irc . users , " ( %s ) relay.relayJoins: How is this possible? %r isn ' t in our user database. " % ( irc . name , user )
2015-09-15 02:23:56 +02:00
u = getRemoteUser ( irc , remoteirc , user )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if u not in remoteirc . channels [ remotechan ] . users :
2016-02-28 03:36:20 +01:00
# Note: only join users if they aren't already joined. This prevents op floods
# on charybdis from repeated SJOINs sent for one user.
# Fetch the known channel TS and all the prefix modes for each user. This ensures
# the different sides of the relay are merged properly.
2015-09-15 02:23:56 +02:00
ts = irc . channels [ channel ] . ts
prefixes = getPrefixModes ( irc , remoteirc , channel , user )
2016-02-28 03:36:20 +01:00
# proto.sjoin() takes its users as a list of (prefix mode characters, UID) pairs.
2015-09-15 02:23:56 +02:00
userpair = ( prefixes , u )
queued_users . append ( userpair )
2016-02-28 03:36:20 +01:00
2015-09-15 02:23:56 +02:00
if queued_users :
2016-02-28 03:36:20 +01:00
# Look at whether we should relay this join as a regular JOIN, or a SJOIN.
# SJOIN will be used if either the amount of users to join is > 1, or there are modes
# to be set on the joining user.
2015-09-15 02:23:56 +02:00
if burst or len ( queued_users ) > 1 or queued_users [ 0 ] [ 0 ] :
2016-02-28 03:36:20 +01:00
# Send the SJOIN from the relay subserver on the target network.
2015-09-15 02:23:56 +02:00
rsid = getRemoteSid ( remoteirc , irc )
2016-01-17 01:53:46 +01:00
remoteirc . proto . sjoin ( rsid , remotechan , queued_users , ts = ts )
2015-09-15 02:23:56 +02:00
relayModes ( irc , remoteirc , getRemoteSid ( irc , remoteirc ) , channel , irc . channels [ channel ] . modes )
else :
2016-02-28 03:36:20 +01:00
# A regular JOIN only needs the user and the channel. TS, source SID, etc., can all be omitted.
2016-01-17 01:36:45 +01:00
remoteirc . proto . join ( queued_users [ 0 ] [ 1 ] , remotechan )
2015-07-20 08:49:50 +02:00
2015-09-15 02:23:56 +02:00
def relayPart ( irc , channel , user ) :
2016-02-28 03:36:20 +01:00
"""
Relays a user part from a channel to its relay links , as part of a channel delink .
"""
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
if name == irc . name or not remoteirc . connected . is_set ( ) :
# Don't relay things to their source network...
2015-08-18 11:44:27 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayPart: looking for %s / %s on %s ' , irc . name , user , irc . name , remoteirc . name )
log . debug ( ' ( %s ) relay.relayPart: remotechan found as %s ' , irc . name , remotechan )
2015-09-15 02:23:56 +02:00
remoteuser = getRemoteUser ( irc , remoteirc , user , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayPart: remoteuser for %s / %s found as %s ' , irc . name , user , irc . name , remoteuser )
2015-09-15 02:23:56 +02:00
if remotechan is None or remoteuser is None :
2016-02-28 03:36:20 +01:00
# If there is no relay channel on the target network, or the relay
# user doesn't exist, just do nothing.
2015-09-15 02:23:56 +02:00
continue
2016-02-28 03:36:20 +01:00
# Part the relay client with the channel delinked message.
2016-01-17 01:51:54 +01:00
remoteirc . proto . part ( remoteuser , remotechan , ' Channel delinked. ' )
2016-02-28 03:36:20 +01:00
# If the relay client no longer has any channels, quit them to prevent inflating /lusers.
2015-09-15 02:23:56 +02:00
if isRelayClient ( remoteirc , remoteuser ) and not remoteirc . users [ remoteuser ] . channels :
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( remoteuser , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , user ) ] [ remoteirc . name ]
2015-07-16 20:53:40 +02:00
2015-07-20 00:56:29 +02:00
whitelisted_cmodes = { ' admin ' , ' allowinvite ' , ' autoop ' , ' ban ' , ' banexception ' ,
' blockcolor ' , ' halfop ' , ' invex ' , ' inviteonly ' , ' key ' ,
' limit ' , ' moderated ' , ' noctcp ' , ' noextmsg ' , ' nokick ' ,
' noknock ' , ' nonick ' , ' nonotice ' , ' op ' , ' operonly ' ,
' opmoderated ' , ' owner ' , ' private ' , ' regonly ' ,
2015-07-25 05:55:48 +02:00
' regmoderated ' , ' secret ' , ' sslonly ' , ' adminonly ' ,
2015-07-20 00:56:29 +02:00
' stripcolor ' , ' topiclock ' , ' voice ' }
2015-07-20 07:43:26 +02:00
whitelisted_umodes = { ' bot ' , ' hidechans ' , ' hideoper ' , ' invisible ' , ' oper ' ,
2015-11-22 08:57:24 +01:00
' regdeaf ' , ' u_stripcolor ' , ' u_noctcp ' , ' wallops ' ,
2015-11-22 22:08:31 +01:00
' hideidle ' }
2015-07-15 04:39:49 +02:00
def relayModes ( irc , remoteirc , sender , channel , modes = None ) :
2016-02-28 03:36:20 +01:00
"""
Relays a mode change on a channel to its relay links .
"""
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayModes: remotechan for %s on %s is %s ' , irc . name , channel , irc . name , remotechan )
2015-07-15 04:39:49 +02:00
if remotechan is None :
return
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
if modes is None :
2015-08-16 04:53:09 +02:00
modes = irc . channels [ channel ] . modes
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayModes: channel data for %s %s : %s ' , irc . name , remoteirc . name , remotechan , remoteirc . channels [ remotechan ] )
2015-07-15 04:39:49 +02:00
supported_modes = [ ]
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayModes: initial modelist for %s is %s ' , irc . name , channel , modes )
2015-07-15 04:39:49 +02:00
for modepair in modes :
try :
prefix , modechar = modepair [ 0 ]
except ValueError :
modechar = modepair [ 0 ]
prefix = ' + '
arg = modepair [ 1 ]
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
# Iterate over every mode see whether the remote IRCd supports
# this mode, and what its mode char for it is (if it is different).
for name , m in irc . cmodes . items ( ) :
supported_char = None
if modechar == m :
2015-07-22 08:53:29 +02:00
supported_char = remoteirc . cmodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-07-22 08:53:29 +02:00
if supported_char is None :
break
2016-02-28 03:36:20 +01:00
2015-07-20 00:56:29 +02:00
if name not in whitelisted_cmodes :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: skipping mode ( %r , %r ) because "
2015-07-20 00:56:29 +02:00
" it isn ' t a whitelisted (safe) mode for relay. " ,
irc . name , modechar , arg )
break
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
if modechar in irc . prefixmodes :
# This is a prefix mode (e.g. +o). We must coerse the argument
# so that the target exists on the remote relay network.
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: coersing argument of ( %r , %r ) "
2015-07-22 07:14:53 +02:00
" for network %r . " ,
irc . name , modechar , arg , remoteirc . name )
2016-02-28 03:36:20 +01:00
2015-07-22 08:47:06 +02:00
# If the target is a remote user, get the real target
# (original user).
2015-09-15 02:29:37 +02:00
arg = getOrigUser ( irc , arg , targetirc = remoteirc ) or \
2015-07-22 08:47:06 +02:00
getRemoteUser ( irc , remoteirc , arg , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: argument found as ( %r , %r ) "
2015-07-22 08:47:06 +02:00
" for network %r . " ,
irc . name , modechar , arg , remoteirc . name )
2016-03-20 01:25:04 +01:00
oplist = remoteirc . channels [ remotechan ] . prefixmodes [ name ]
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: 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 :
2016-02-28 03:36:20 +01:00
2015-07-22 08:53:29 +02:00
# Don't set prefix modes that are already set.
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: skipping setting %s on %s / %s because it appears to be already set. " ,
2015-07-22 08:53:29 +02:00
irc . name , name , arg , remoteirc . name )
break
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
supported_char = remoteirc . cmodes . get ( name )
2016-02-28 03:36:20 +01:00
2015-07-15 04:39:49 +02:00
if supported_char :
2015-07-25 19:43:47 +02:00
final_modepair = ( prefix + supported_char , arg )
2015-07-20 00:20:23 +02:00
if name in ( ' ban ' , ' banexception ' , ' invex ' ) and not utils . isHostmask ( arg ) :
2016-02-28 03:36:20 +01:00
2015-07-20 00:20:23 +02:00
# Don't add bans that don't match n!u@h syntax!
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: skipping mode ( %r , %r ) because it doesn ' t match nick!user@host syntax. " ,
2015-07-20 00:56:29 +02:00
irc . name , modechar , arg )
break
2016-02-28 03:36:20 +01:00
2015-07-22 08:53:29 +02:00
# Don't set modes that are already set, to prevent floods on TS6
# where the same mode can be set infinite times.
2015-07-25 19:43:47 +02:00
if prefix == ' + ' and final_modepair in remoteirc . channels [ remotechan ] . modes :
2016-02-28 03:36:20 +01:00
log . debug ( " ( %s ) relay.relayModes: skipping setting mode ( %r , %r ) on %s %s because it appears to be already set. " ,
2015-07-22 08:53:29 +02:00
irc . name , supported_char , arg , remoteirc . name , remotechan )
break
2016-02-28 03:36:20 +01:00
2015-07-25 19:43:47 +02:00
supported_modes . append ( final_modepair )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.relayModes: final modelist (sending to %s %s ) is %s ' , irc . name , remoteirc . name , remotechan , supported_modes )
2015-07-17 01:27:17 +02:00
# Don't send anything if there are no supported modes left after filtering.
if supported_modes :
# Check if the sender is a user; remember servers are allowed to set modes too.
2015-08-21 07:13:28 +02:00
u = getRemoteUser ( irc , remoteirc , sender , spawnIfMissing = False )
if u :
2016-01-17 02:08:17 +01:00
remoteirc . proto . mode ( u , remotechan , supported_modes )
2015-07-22 07:14:53 +02:00
else :
2015-09-12 21:06:58 +02:00
rsid = getRemoteSid ( remoteirc , irc )
2016-01-17 02:08:17 +01:00
remoteirc . proto . mode ( rsid , remotechan , supported_modes )
2015-07-15 04:39:49 +02:00
2015-09-15 02:23:56 +02:00
def relayWhoisHandler ( irc , target ) :
2016-02-28 03:36:20 +01:00
"""
WHOIS handler for the relay plugin .
"""
2015-09-15 02:23:56 +02:00
user = irc . users [ target ]
2015-09-15 02:29:37 +02:00
orig = getOrigUser ( irc , target )
2015-09-15 02:23:56 +02:00
if orig :
network , remoteuid = orig
remotenick = world . networkobjects [ network ] . users [ remoteuid ] . nick
return [ 320 , " %s :is a remote user connected via PyLink Relay. Home "
" network: %s ; Home nick: %s " % ( user . nick , network ,
remotenick ) ]
world . whois_handlers . append ( relayWhoisHandler )
### GENERIC EVENT HOOK HANDLERS
def handle_operup ( irc , numeric , command , args ) :
newtype = args [ ' text ' ] + ' _(remote) '
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
2015-09-20 20:25:45 +02:00
log . debug ( ' ( %s ) relay.handle_opertype: setting OPERTYPE of %s / %s to %s ' ,
irc . name , user , netname , newtype )
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ netname ]
remoteirc . users [ user ] . opertype = newtype
2015-12-27 00:41:22 +01:00
utils . add_hook ( handle_operup , ' CLIENT_OPERED ' )
2015-09-15 02:23:56 +02:00
def handle_join ( irc , numeric , command , args ) :
channel = args [ ' channel ' ]
2015-09-15 02:29:37 +02:00
if not getRelay ( ( irc . name , channel ) ) :
2015-09-15 02:23:56 +02:00
# No relay here, return.
return
ts = args [ ' ts ' ]
users = set ( args [ ' users ' ] )
relayJoins ( irc , channel , users , ts , burst = False )
utils . add_hook ( handle_join , ' JOIN ' )
def handle_quit ( irc , numeric , command , args ) :
for netname , user in relayusers [ ( irc . name , numeric ) ] . copy ( ) . items ( ) :
remoteirc = world . networkobjects [ netname ]
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( user , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , numeric ) ]
utils . add_hook ( handle_quit , ' QUIT ' )
def handle_squit ( irc , numeric , command , args ) :
users = args [ ' users ' ]
target = args [ ' target ' ]
# 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 ]
for userpair in relayusers :
if userpair [ 0 ] == remotenet and irc . name in relayusers [ userpair ] :
del relayusers [ userpair ] [ irc . name ]
remoteirc = world . networkobjects [ remotenet ]
initializeAll ( remoteirc )
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 )
2015-09-15 02:23:56 +02:00
handle_quit ( irc , user , command , { ' text ' : ' *.net *.split ' } )
utils . add_hook ( handle_squit , ' SQUIT ' )
def handle_nick ( irc , numeric , command , args ) :
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
newnick = normalizeNick ( remoteirc , irc . name , args [ ' newnick ' ] , uid = user )
if remoteirc . users [ user ] . nick != newnick :
2016-01-17 01:51:04 +01:00
remoteirc . proto . nick ( user , newnick )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_nick , ' NICK ' )
def handle_part ( irc , numeric , command , args ) :
channels = args [ ' channels ' ]
text = args [ ' text ' ]
# Don't allow the PyLink client PARTing to be relayed.
if numeric == irc . pseudoclient . uid :
return
for channel in channels :
for netname , user in relayusers [ ( irc . name , numeric ) ] . copy ( ) . items ( ) :
remoteirc = world . networkobjects [ netname ]
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-15 02:23:56 +02:00
if remotechan is None :
continue
2016-01-17 01:51:54 +01:00
remoteirc . proto . part ( user , remotechan , text )
2015-09-15 02:23:56 +02:00
if not remoteirc . users [ user ] . channels :
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( user , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
del relayusers [ ( irc . name , numeric ) ] [ remoteirc . name ]
utils . add_hook ( handle_part , ' PART ' )
2015-09-26 19:10:54 +02:00
def handle_messages ( irc , numeric , command , args ) :
notice = ( command in ( ' NOTICE ' , ' PYLINK_SELF_NOTICE ' ) )
2015-09-15 02:23:56 +02:00
target = args [ ' target ' ]
text = args [ ' text ' ]
2016-01-01 02:28:47 +01:00
if irc . isInternalClient ( numeric ) and irc . isInternalClient ( target ) :
2015-11-27 07:51:19 +01:00
# Drop attempted PMs between internal clients (this shouldn't happen,
# but whatever).
return
2016-02-21 04:29:52 +01:00
2015-11-27 07:51:19 +01:00
elif numeric in irc . servers :
# Sender is a server? This shouldn't be allowed, except for some truly
# special cases... We'll route these through the main PyLink client,
# tagging the message with the sender name.
text = ' [from %s ] %s ' % ( irc . servers [ numeric ] . name , text )
numeric = irc . pseudoclient . uid
2016-02-21 04:29:52 +01:00
2015-11-27 07:51:19 +01:00
elif numeric not in irc . users :
# Sender didn't pass the check above, AND isn't a user.
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_messages: Unknown message sender %s . ' , irc . name , numeric )
2015-09-15 02:23:56 +02:00
return
2016-02-21 04:29:52 +01:00
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , target ) )
2015-09-15 02:23:56 +02:00
remoteusers = relayusers [ ( irc . name , numeric ) ]
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
# HACK: Don't break on sending to @#channel or similar.
try :
prefix , target = target . split ( ' # ' , 1 )
except ValueError :
prefix = ' '
else :
target = ' # ' + target
2016-02-21 04:29:52 +01:00
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_messages: prefix is %r , target is %r ' , irc . name , prefix , target )
2015-09-15 02:23:56 +02:00
if utils . isChannel ( target ) and relay and numeric not in irc . channels [ target ] . users :
# The sender must be in the target channel to send messages over the relay;
# it's the only way we can make sure they have a spawned client on ALL
# of the linked networks. This affects -n channels too; see
# https://github.com/GLolol/PyLink/issues/91 for an explanation of why.
irc . msg ( numeric , ' Error: You must be in %r in order to send '
' messages over the relay. ' % target , notice = True )
return
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
if utils . isChannel ( target ) :
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:29:37 +02:00
real_target = getRemoteChan ( irc , remoteirc , target )
2016-02-21 04:29:52 +01:00
# Don't relay anything back to the source net, or to disconnected networks
# and networks without a relay for this channel
if irc . name == name or ( not remoteirc . connected . is_set ( ) ) or ( not real_target ) \
or ( not irc . connected . is_set ( ) ) :
2015-09-15 02:23:56 +02:00
continue
2016-02-21 04:29:52 +01:00
2015-09-26 19:10:54 +02:00
user = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
2015-09-15 02:23:56 +02:00
real_target = prefix + real_target
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_messages: sending message to %s from %s on behalf of %s ' ,
2016-02-21 04:29:52 +01:00
irc . name , real_target , user , numeric )
2015-09-15 02:23:56 +02:00
if notice :
2016-01-17 01:44:23 +01:00
remoteirc . proto . notice ( user , real_target , text )
2015-09-15 02:23:56 +02:00
else :
2016-01-17 01:44:23 +01:00
remoteirc . proto . message ( user , real_target , text )
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
else :
2016-02-21 04:29:52 +01:00
# Get the real user that the PM was meant for
origuser = getOrigUser ( irc , target )
if origuser is None : # Not a relay client, return
2015-09-15 02:23:56 +02:00
return
2016-02-21 04:29:52 +01:00
homenet , real_target = origuser
2015-09-15 02:23:56 +02:00
# For PMs, we must be on a common channel with the target.
# Otherwise, the sender doesn't have a client representing them
# on the remote network, and we won't have anything to send our
# messages from.
if homenet not in remoteusers . keys ( ) :
irc . msg ( numeric , ' Error: You must be in a common channel '
' with %r in order to send messages. ' % \
irc . users [ target ] . nick , notice = True )
return
remoteirc = world . networkobjects [ homenet ]
user = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
2016-02-21 04:29:52 +01:00
2015-09-15 02:23:56 +02:00
if notice :
2016-01-17 01:44:23 +01:00
remoteirc . proto . notice ( user , real_target , text )
2015-07-20 07:43:26 +02:00
else :
2016-01-17 01:44:23 +01:00
remoteirc . proto . message ( user , real_target , text )
2016-02-21 04:29:52 +01:00
2015-09-26 19:10:54 +02:00
for cmd in ( ' PRIVMSG ' , ' NOTICE ' , ' PYLINK_SELF_NOTICE ' , ' PYLINK_SELF_PRIVMSG ' ) :
utils . add_hook ( handle_messages , cmd )
2015-09-15 02:23:56 +02:00
def handle_kick ( irc , source , command , args ) :
channel = args [ ' channel ' ]
target = args [ ' target ' ]
text = args [ ' text ' ]
kicker = source
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-09-15 02:23:56 +02:00
# Don't allow kicks to the PyLink client to be relayed.
if relay is None or target == irc . pseudoclient . uid :
return
2015-09-15 02:29:37 +02:00
origuser = getOrigUser ( irc , target )
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-15 02:23:56 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
continue
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: remotechan for %s on %s is %s ' , irc . name , channel , name , remotechan )
2015-09-15 02:23:56 +02:00
if remotechan is None :
continue
real_kicker = getRemoteUser ( irc , remoteirc , kicker , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: real kicker for %s on %s is %s ' , irc . name , kicker , name , real_kicker )
2015-09-15 02:23:56 +02:00
if not isRelayClient ( irc , target ) :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: target %s is NOT an internal client ' , irc . name , target )
2015-09-15 02:23:56 +02:00
# Both the target and kicker are external clients; i.e.
# they originate from the same network. We won't have
# to filter this; the uplink IRCd will handle it appropriately,
# and we'll just follow.
real_target = getRemoteUser ( irc , remoteirc , target , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: real target for %s is %s ' , irc . name , target , real_target )
2015-09-15 02:23:56 +02:00
else :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: target %s is an internal client, going to look up the real user ' , irc . name , target )
2015-09-15 02:29:37 +02:00
real_target = getOrigUser ( irc , target , targetirc = remoteirc )
2015-09-15 02:23:56 +02:00
if not checkClaim ( irc , channel , kicker ) :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s ' , irc . name , kicker , real_target )
2015-09-15 02:23:56 +02:00
# Home network is not in the channel's claim AND the kicker is not
# opped. We won't propograte the kick then.
# TODO: make the check slightly more advanced: i.e. halfops can't
# kick ops, admins can't kick owners, etc.
modes = getPrefixModes ( remoteirc , irc , remotechan , real_target )
# Join the kicked client back with its respective modes.
2016-01-17 01:53:46 +01:00
irc . proto . sjoin ( irc . sid , channel , [ ( modes , target ) ] )
2015-09-15 02:23:56 +02:00
if kicker in irc . users :
2016-03-27 01:56:11 +01:00
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from %s / %s to relay client %s / %s on %s . ' ,
irc . name , args [ ' text ' ] , irc . users [ source ] . nick , irc . name ,
remoteirc . users [ real_target ] . nick , remoteirc . name , channel )
2015-09-15 02:23:56 +02:00
irc . msg ( kicker , " This channel is claimed; your kick to "
" %s has been blocked because you are not "
" (half)opped. " % channel , notice = True )
else :
2016-03-27 01:56:11 +01:00
log . info ( ' ( %s ) relay: Blocked KICK (reason %r ) from server %s to relay client %s / %s on %s . ' ,
2015-09-15 02:23:56 +02:00
irc . name , args [ ' text ' ] , irc . servers [ source ] . name ,
remoteirc . users [ real_target ] . nick , remoteirc . name , channel )
return
if not real_target :
continue
# Propogate the kick!
if real_kicker :
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s / %s ' , irc . name , real_target , remotechan , real_kicker , kicker , irc . name )
2016-01-17 01:59:01 +01:00
remoteirc . proto . kick ( real_kicker , remotechan , real_target , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
else :
# Kick originated from a server, or the kicker isn't in any
# common channels with the target relay network.
rsid = getRemoteSid ( remoteirc , irc )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s / %s ' , irc . name , real_target , remotechan , rsid , kicker , irc . name )
2015-09-15 02:23:56 +02:00
try :
if kicker in irc . servers :
kname = irc . servers [ kicker ] . name
else :
kname = irc . users . get ( kicker ) . nick
2015-09-21 03:13:39 +02:00
text = " ( %s / %s ) %s " % ( kname , irc . name , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
except AttributeError :
2015-09-21 03:13:39 +02:00
text = " (<unknown kicker>@ %s ) %s " % ( irc . name , args [ ' text ' ] )
2016-01-17 01:59:01 +01:00
remoteirc . proto . kick ( rsid , remotechan , real_target , text )
2015-09-15 02:23:56 +02:00
# If the target isn't on any channels, quit them.
2015-10-18 19:25:59 +02:00
if remoteirc != irc and ( not remoteirc . users [ real_target ] . channels ) and not origuser :
del relayusers [ ( irc . name , target ) ] [ remoteirc . name ]
2016-01-17 01:51:54 +01:00
remoteirc . proto . quit ( real_target , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
if origuser and not irc . users [ target ] . channels :
del relayusers [ origuser ] [ irc . name ]
2016-01-17 01:51:54 +01:00
irc . proto . quit ( target , ' Left all shared channels. ' )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_kick , ' KICK ' )
def handle_chgclient ( irc , source , command , args ) :
target = args [ ' target ' ]
if args . get ( ' newhost ' ) :
field = ' HOST '
text = args [ ' newhost ' ]
elif args . get ( ' newident ' ) :
field = ' IDENT '
text = args [ ' newident ' ]
elif args . get ( ' newgecos ' ) :
field = ' GECOS '
text = args [ ' newgecos ' ]
if field :
for netname , user in relayusers [ ( irc . name , target ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
try :
2015-11-22 08:37:19 +01:00
if field == ' HOST ' :
text = normalizeHost ( remoteirc , text )
2015-09-15 02:23:56 +02:00
remoteirc . proto . updateClient ( user , field , text )
except NotImplementedError : # IRCd doesn't support changing the field we want
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_chgclient: Ignoring changing field %r of %s on %s (for %s / %s ); '
2015-09-15 02:23:56 +02:00
' remote IRCd doesn \' t support it ' , irc . name , field ,
2015-12-31 00:53:05 +01:00
user , netname , target , irc . name )
2015-09-15 02:23:56 +02:00
continue
for c in ( ' CHGHOST ' , ' CHGNAME ' , ' CHGIDENT ' ) :
utils . add_hook ( handle_chgclient , c )
2015-07-20 07:43:26 +02:00
2015-07-15 04:39:49 +02:00
def handle_mode ( irc , numeric , command , args ) :
target = args [ ' target ' ]
modes = args [ ' modes ' ]
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-07-26 07:56:34 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
2015-07-15 04:39:49 +02:00
continue
2016-02-28 03:36:20 +01:00
2015-07-20 07:43:26 +02:00
if utils . isChannel ( target ) :
2015-09-15 02:36:41 +02:00
oldchan = args . get ( ' oldchan ' )
2016-02-28 03:36:20 +01:00
2015-09-13 22:48:14 +02:00
if checkClaim ( irc , target , numeric , chanobj = oldchan ) :
2015-09-13 08:36:52 +02:00
relayModes ( irc , remoteirc , numeric , target , modes )
else : # Mode change blocked by CLAIM.
2016-05-01 01:33:46 +02:00
reversed_modes = irc . reverseModes ( target , modes , oldobj = oldchan )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_mode: Reversing mode changes of %r with %r (CLAIM). ' ,
2015-09-13 08:36:52 +02:00
irc . name , modes , reversed_modes )
2016-01-17 02:08:17 +01:00
irc . proto . mode ( irc . pseudoclient . uid , target , reversed_modes )
2015-09-13 08:36:52 +02:00
break
2016-02-28 03:36:20 +01:00
2015-07-20 07:43:26 +02:00
else :
2015-08-31 23:23:42 +02:00
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc . umodes . get ( ' hideoper ' )
2015-07-20 07:43:26 +02:00
modes = getSupportedUmodes ( irc , remoteirc , modes )
2016-02-28 03:36:20 +01:00
2015-08-31 23:23:42 +02:00
if hideoper_mode :
if ( ' +o ' , None ) in modes :
modes . append ( ( ' + %s ' % hideoper_mode , None ) )
elif ( ' -o ' , None ) in modes :
modes . append ( ( ' - %s ' % hideoper_mode , None ) )
2016-02-28 03:36:20 +01:00
2015-07-20 08:49:50 +02:00
remoteuser = getRemoteUser ( irc , remoteirc , target , spawnIfMissing = False )
2016-02-28 03:36:20 +01:00
2015-09-02 07:13:29 +02:00
if remoteuser and modes :
2016-01-17 02:08:17 +01:00
remoteirc . proto . mode ( remoteuser , remoteuser , modes )
2015-07-20 07:43:26 +02:00
2015-07-15 04:39:49 +02:00
utils . add_hook ( handle_mode , ' MODE ' )
2015-07-15 08:24:21 +02:00
def handle_topic ( irc , numeric , command , args ) :
channel = args [ ' channel ' ]
2015-09-14 02:58:39 +02:00
oldtopic = args . get ( ' oldtopic ' )
2015-12-19 06:53:35 +01:00
topic = args [ ' text ' ]
2015-09-13 23:23:27 +02:00
if checkClaim ( irc , channel , numeric ) :
2016-03-27 03:19:08 +02:00
for name , remoteirc in world . networkobjects . copy ( ) . items ( ) :
2015-09-13 23:23:27 +02:00
if irc . name == name or not remoteirc . connected . is_set ( ) :
continue
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-13 23:23:27 +02:00
# Don't send if the remote topic is the same as ours.
if remotechan is None or topic == remoteirc . channels [ remotechan ] . topic :
continue
# This might originate from a server too.
remoteuser = getRemoteUser ( irc , remoteirc , numeric , spawnIfMissing = False )
if remoteuser :
2016-01-17 02:09:52 +01:00
remoteirc . proto . topic ( remoteuser , remotechan , topic )
2015-09-13 23:23:27 +02:00
else :
rsid = getRemoteSid ( remoteirc , irc )
2016-01-17 02:09:52 +01:00
remoteirc . proto . topicBurst ( rsid , remotechan , topic )
2015-09-14 02:58:39 +02:00
elif oldtopic : # Topic change blocked by claim.
2016-01-17 02:09:52 +01:00
irc . proto . topic ( irc . pseudoclient . uid , channel , oldtopic )
2015-07-15 08:24:21 +02:00
utils . add_hook ( handle_topic , ' TOPIC ' )
2015-07-15 08:25:40 +02:00
def handle_kill ( irc , numeric , command , args ) :
target = args [ ' target ' ]
userdata = args [ ' userdata ' ]
2016-03-08 03:10:36 +01:00
# Try to find the original client of the target being killed
if userdata and hasattr ( userdata , ' remote ' ) :
realuser = userdata . remote
else :
realuser = getOrigUser ( irc , target )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_kill: realuser is %r ' , irc . name , realuser )
2016-03-08 03:10:36 +01:00
2015-07-25 03:26:31 +02:00
# Target user was remote:
if realuser and realuser [ 0 ] != irc . name :
# We don't allow killing over the relay, so we must respawn the affected
# client and rejoin it to its channels.
del relayusers [ realuser ] [ irc . name ]
2016-03-26 00:39:06 +01:00
remoteirc = world . networkobjects [ realuser [ 0 ] ]
for remotechan in remoteirc . users [ realuser [ 1 ] ] . channels :
localchan = getRemoteChan ( remoteirc , irc , remotechan )
if localchan :
modes = getPrefixModes ( remoteirc , irc , remotechan , realuser [ 1 ] )
log . debug ( ' ( %s ) relay.handle_kill: userpair: %s , %s ' , irc . name , modes , realuser )
client = getRemoteUser ( remoteirc , irc , realuser [ 1 ] )
irc . proto . sjoin ( getRemoteSid ( irc , remoteirc ) , localchan , [ ( modes , client ) ] )
if userdata and numeric in irc . users :
log . info ( ' ( %s ) relay.handle_kill: Blocked KILL (reason %r ) from %s to relay client %s / %s . ' ,
irc . name , args [ ' text ' ] , irc . users [ numeric ] . nick ,
remoteirc . users [ realuser [ 1 ] ] . nick , realuser [ 0 ] )
irc . msg ( numeric , " Your kill to %s has been blocked "
" because PyLink does not allow killing "
" users over the relay at this time. " % \
userdata . nick , notice = True )
2015-08-23 06:43:25 +02:00
else :
2016-03-26 00:39:06 +01:00
log . info ( ' ( %s ) relay.handle_kill: Blocked KILL (reason %r ) from server %s to relay client %s / %s . ' ,
irc . name , args [ ' text ' ] , irc . servers [ numeric ] . name ,
remoteirc . users [ realuser [ 1 ] ] . nick , realuser [ 0 ] )
2015-09-13 02:41:49 +02:00
2015-07-25 03:26:31 +02:00
# Target user was local.
else :
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages
# for locally killed clients, while others (inspircd) do!
# If we receive a user object in 'userdata' instead of None, it means
# that the KILL hasn't been handled by a preceding QUIT message.
if userdata :
handle_quit ( irc , target , ' KILL ' , { ' text ' : args [ ' text ' ] } )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_kill , ' KILL ' )
def handle_away ( irc , numeric , command , args ) :
for netname , user in relayusers [ ( irc . name , numeric ) ] . items ( ) :
remoteirc = world . networkobjects [ netname ]
2016-01-17 01:40:36 +01:00
remoteirc . proto . away ( user , args [ ' text ' ] )
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_away , ' AWAY ' )
def handle_invite ( irc , source , command , args ) :
target = args [ ' target ' ]
channel = args [ ' channel ' ]
if isRelayClient ( irc , target ) :
2015-09-15 02:29:37 +02:00
remotenet , remoteuser = getOrigUser ( irc , target )
2015-09-15 02:23:56 +02:00
remoteirc = world . networkobjects [ remotenet ]
2015-09-15 02:29:37 +02:00
remotechan = getRemoteChan ( irc , remoteirc , channel )
2015-09-15 02:23:56 +02:00
remotesource = getRemoteUser ( irc , remoteirc , source , spawnIfMissing = False )
if remotesource is None :
irc . msg ( source , ' Error: You must be in a common channel '
' with %s to invite them to channels. ' % \
irc . users [ target ] . nick ,
notice = True )
elif remotechan is None :
irc . msg ( source , ' Error: You cannot invite someone to a '
' channel not on their network! ' ,
notice = True )
else :
2016-01-17 01:38:27 +01:00
remoteirc . proto . invite ( remotesource , remoteuser ,
2015-09-15 02:23:56 +02:00
remotechan )
utils . add_hook ( handle_invite , ' INVITE ' )
2015-09-29 03:15:56 +02:00
def handle_endburst ( irc , numeric , command , args ) :
if numeric == irc . uplink :
initializeAll ( irc )
utils . add_hook ( handle_endburst , " ENDBURST " )
2015-09-15 02:23:56 +02:00
def handle_disconnect ( irc , numeric , command , args ) :
2015-10-11 00:34:57 +02:00
""" Handles IRC network disconnections (internal hook). """
# Quit all of our users' representations on other nets, and remove
# them from our relay clients index.
2016-03-31 03:33:44 +02:00
with spawnlocks [ irc . name ] :
for k , v in relayusers . copy ( ) . items ( ) :
if irc . name in v :
del relayusers [ k ] [ irc . name ]
if k [ 0 ] == irc . name :
try :
handle_quit ( irc , k [ 1 ] , ' PYLINK_DISCONNECT ' , { ' text ' : ' Relay network lost connection. ' } )
del relayusers [ k ]
except KeyError :
pass
2015-10-11 00:34:57 +02:00
# SQUIT all relay pseudoservers spawned for us, and remove them
# from our relay subservers index.
2016-03-31 03:33:44 +02:00
with spawnlocks_servers [ irc . name ] :
for name , ircobj in world . networkobjects . copy ( ) . items ( ) :
if name != irc . name and ircobj . connected . is_set ( ) :
try :
rsid = relayservers [ name ] [ irc . name ]
except KeyError :
continue
else :
ircobj . proto . squit ( ircobj . sid , rsid , text = ' Relay network lost connection. ' )
2015-10-11 00:34:57 +02:00
try :
2016-03-31 03:33:44 +02:00
del relayservers [ name ] [ irc . name ]
2015-10-11 00:34:57 +02:00
except KeyError :
2016-03-31 03:33:44 +02:00
pass
2016-03-25 23:59:37 +01:00
try :
2016-03-31 03:33:44 +02:00
del relayservers [ irc . name ]
2016-03-25 23:59:37 +01:00
except KeyError :
pass
2015-09-15 02:23:56 +02:00
utils . add_hook ( handle_disconnect , " PYLINK_DISCONNECT " )
2015-07-13 08:28:54 +02:00
2015-09-15 02:23:56 +02:00
def handle_save ( irc , numeric , command , args ) :
target = args [ ' target ' ]
2015-09-15 02:29:37 +02:00
realuser = getOrigUser ( irc , target )
2016-02-28 03:36:20 +01:00
log . debug ( ' ( %s ) relay.handle_save: %r got in a nick collision! Real user: %r ' ,
2015-09-15 02:23:56 +02:00
irc . name , target , realuser )
if isRelayClient ( irc , target ) and realuser :
# Nick collision!
# It's one of our relay clients; try to fix our nick to the next
# available normalized nick.
remotenet , remoteuser = realuser
remoteirc = world . networkobjects [ remotenet ]
nick = remoteirc . users [ remoteuser ] . nick
2016-03-26 00:39:06 +01:00
newnick = normalizeNick ( irc , remotenet , nick )
log . info ( ' ( %s ) relay.handle_save: SAVE received for relay client %r ( %s ), fixing nick to %s ' ,
irc . name , target , nick , newnick )
irc . proto . nick ( target , newnick )
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 " )
### PUBLIC COMMANDS
2015-07-13 04:03:18 +02:00
2015-07-11 05:26:46 +02:00
@utils.add_cmd
def create ( irc , source , args ) :
""" <channel>
Creates the channel < channel > over the relay . """
try :
2015-09-02 07:01:22 +02:00
channel = utils . toLower ( irc , args [ 0 ] )
2015-07-11 05:26:46 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1: channel. " )
2015-07-12 22:09:35 +02:00
return
2015-07-11 05:26:46 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2015-07-13 02:59:09 +02:00
if source not in irc . channels [ channel ] . users :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: You must be in %r to complete this operation. ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-11-29 06:09:16 +01:00
# Check to see whether the channel requested is already part of a different
# relay.
2015-09-18 04:25:51 +02:00
localentry = getRelay ( ( irc . name , channel ) )
2015-09-18 04:24:38 +02:00
if localentry :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Channel %r is already part of a relay. ' % channel )
2015-09-18 04:24:38 +02:00
return
2015-11-29 06:09:16 +01:00
2016-05-01 01:44:37 +02:00
creator = irc . getHostmask ( source )
2015-11-29 06:09:16 +01:00
# Create the relay database entry with the (network name, channel name)
# pair - this is just a dict with various keys.
db [ ( irc . name , channel ) ] = { ' claim ' : [ irc . name ] , ' links ' : set ( ) ,
2016-01-10 05:30:54 +01:00
' blocked_nets ' : set ( ) , ' creator ' : creator ,
' ts ' : time . time ( ) }
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s created by %s . ' , irc . name , channel , creator )
2015-07-13 08:28:54 +02:00
initializeChannel ( irc , channel )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-07-11 05:26:46 +02:00
@utils.add_cmd
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-03-05 18:31:59 +01:00
Removes < channel > from the relay , delinking all networks linked to it . If < home network > is given and you are logged in as admin , this can also remove relay channels from other networks . """
try : # Two args were given: first one is network name, second is channel.
channel = utils . toLower ( irc , args [ 1 ] )
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.
channel = utils . toLower ( irc , args [ 0 ] )
network = irc . name
except IndexError :
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, network (optional). " )
return
2015-07-11 05:26:46 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-11 05:26:46 +02:00
return
2016-03-05 18:31:59 +01:00
if network == irc . name :
# If we're destroying a channel on the current network, only oper is needed.
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2016-03-05 18:31:59 +01:00
else :
# Otherwise, we'll need to be logged in as admin.
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source , allowOper = False )
2016-03-05 18:31:59 +01:00
entry = ( network , channel )
2015-07-14 08:29:20 +02:00
if entry in db :
2016-03-05 18:31:59 +01:00
# Iterate over all the channel links and deinitialize them.
2015-07-14 08:29:20 +02:00
for link in db [ entry ] [ ' links ' ] :
2015-08-29 18:39:33 +02:00
removeChannel ( world . networkobjects . get ( link [ 0 ] ) , link [ 1 ] )
2016-03-05 18:31:59 +01:00
removeChannel ( world . networkobjects . get ( network ) , channel )
2015-07-14 08:29:20 +02:00
del db [ entry ]
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s destroyed by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
channel , irc . getHostmask ( source ) )
2016-03-05 18:31:59 +01:00
irc . reply ( ' Done. ' )
2015-07-11 05:26:46 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-07-13 02:59:09 +02:00
return
2015-07-11 05:26:46 +02:00
2015-07-13 02:59:09 +02:00
@utils.add_cmd
def link ( irc , source , args ) :
""" <remotenet> <channel> <local channel>
Links channel < channel > on < remotenet > over the relay to < local channel > .
2015-07-18 07:35:34 +02:00
If < local channel > is not specified , it defaults to the same name as < channel > . """
2015-07-13 02:59:09 +02:00
try :
2015-09-02 07:01:22 +02:00
channel = utils . toLower ( irc , args [ 1 ] )
2016-04-02 21:15:53 +02:00
remotenet = args [ 0 ]
2015-07-13 02:59:09 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional). " )
2015-07-13 02:59:09 +02:00
return
try :
2015-09-02 07:01:22 +02:00
localchan = utils . toLower ( irc , args [ 2 ] )
2015-07-13 02:59:09 +02:00
except IndexError :
localchan = channel
for c in ( channel , localchan ) :
if not utils . isChannel ( c ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % c )
2015-07-13 02:59:09 +02:00
return
if source not in irc . channels [ localchan ] . users :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: You must be in %r to complete this operation. ' % localchan )
2015-07-13 02:59:09 +02:00
return
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-08-29 18:39:33 +02:00
if remotenet not in world . networkobjects :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No network named %r exists. ' % remotenet )
2015-07-13 02:59:09 +02:00
return
2015-09-15 02:29:37 +02:00
localentry = getRelay ( ( irc . name , localchan ) )
2015-07-14 07:54:51 +02:00
if localentry :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Channel %r is already part of a relay. ' % localchan )
2015-07-13 02:59:09 +02:00
return
try :
entry = db [ ( remotenet , channel ) ]
except KeyError :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-07-13 02:59:09 +02:00
return
else :
2015-08-26 05:18:14 +02:00
if irc . name in entry [ ' blocked_nets ' ] :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Access denied (network is banned from linking to this channel). ' )
2015-08-26 05:18:14 +02:00
return
2015-07-14 07:54:51 +02:00
for link in entry [ ' links ' ] :
if link [ 0 ] == irc . name :
2015-11-29 06:18:30 +01:00
irc . reply ( " Error: Remote channel ' %s %s ' is already linked here "
" as %r . " % ( remotenet , channel , link [ 1 ] ) )
2015-07-14 07:54:51 +02:00
return
2015-07-13 02:59:09 +02:00
entry [ ' links ' ] . add ( ( irc . name , localchan ) )
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s linked to %s %s by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
localchan , remotenet , channel , irc . getHostmask ( source ) )
2015-07-13 08:28:54 +02:00
initializeChannel ( irc , localchan )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-07-12 22:09:35 +02:00
2015-07-13 02:59:09 +02:00
@utils.add_cmd
def delink ( irc , source , args ) :
""" <local channel> [<network>]
2015-07-18 07:35:34 +02:00
Delinks channel < local channel > . < network > must and can only be specified if you are on the host network for < local channel > , and allows you to pick which network to delink .
To remove a relay entirely , use the ' destroy ' command instead . """
2015-07-13 02:59:09 +02:00
try :
2015-09-02 07:01:22 +02:00
channel = utils . toLower ( irc , args [ 0 ] )
2015-07-13 02:59:09 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, remote netname (optional). " )
2015-07-13 02:59:09 +02:00
return
try :
2016-04-02 21:15:53 +02:00
remotenet = args [ 1 ]
2015-07-13 02:59:09 +02:00
except IndexError :
remotenet = None
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-07-13 02:59:09 +02:00
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-07-13 02:59:09 +02:00
return
2015-09-15 02:29:37 +02:00
entry = getRelay ( ( irc . name , channel ) )
2015-07-14 07:54:51 +02:00
if entry :
if entry [ 0 ] == irc . name : # We own this channel.
2015-07-26 01:56:20 +02:00
if not remotenet :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: You must select a network to "
2015-08-12 16:03:49 +02:00
" delink, or use the ' destroy ' command to remove "
" this relay entirely (it was created on the current "
" network). " )
2015-07-14 07:54:51 +02:00
return
else :
2015-07-15 20:47:06 +02:00
for link in db [ entry ] [ ' links ' ] . copy ( ) :
if link [ 0 ] == remotenet :
2015-08-29 18:39:33 +02:00
removeChannel ( world . networkobjects . get ( remotenet ) , link [ 1 ] )
2015-07-15 20:47:06 +02:00
db [ entry ] [ ' links ' ] . remove ( link )
2015-07-13 02:59:09 +02:00
else :
2015-07-14 07:54:51 +02:00
removeChannel ( irc , channel )
2015-07-14 08:29:20 +02:00
db [ entry ] [ ' links ' ] . remove ( ( irc . name , channel ) )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-11-29 06:18:30 +01:00
log . info ( ' ( %s ) relay: Channel %s delinked from %s %s by %s . ' , irc . name ,
2016-05-01 01:44:37 +02:00
channel , entry [ 0 ] , entry [ 1 ] , irc . getHostmask ( source ) )
2015-07-13 02:59:09 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r . ' % channel )
2015-07-13 02:59:09 +02:00
2015-07-18 07:00:25 +02:00
@utils.add_cmd
def linked ( irc , source , args ) :
2015-07-18 07:35:34 +02:00
""" takes no arguments.
Returns a list of channels shared across the relay . """
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 ) )
2015-09-07 07:23:44 +02:00
irc . msg ( source , s )
2015-11-29 06:09:16 +01:00
# Sort the list of shared channels when displaying
for k , v in sorted ( db . items ( ) ) :
2016-02-22 05:04:53 +01:00
2015-11-29 06:09:16 +01:00
# Bold each network/channel name pair
2015-07-18 07:00:25 +02:00
s = ' \x02 %s %s \x02 ' % k
2015-09-19 07:05:51 +02:00
remoteirc = world . networkobjects . get ( k [ 0 ] )
2015-11-29 06:09:16 +01:00
channel = k [ 1 ] # Get the channel name from the network/channel pair
2016-02-22 05:04:53 +01:00
2015-09-19 07:05:51 +02:00
if remoteirc and channel in remoteirc . channels :
c = remoteirc . channels [ channel ]
if ( ' s ' , None ) in c . modes or ( ' p ' , None ) in c . modes :
2015-11-29 06:09:16 +01:00
# Only show secret channels to opers, and tag them with
# [secret].
2016-05-01 01:54:11 +02:00
if irc . isOper ( source ) :
2015-09-19 07:05:51 +02:00
s + = ' \x02 [secret] \x02 '
else :
continue
2015-11-29 06:09:16 +01:00
if v [ ' links ' ] : # Join up and output all the linked channel names.
2016-01-20 16:14:49 +01:00
s + = ' ' . join ( [ ' ' . join ( link ) for link in sorted ( v [ ' links ' ] ) ] )
2016-02-22 05:04:53 +01:00
2015-11-29 06:09:16 +01:00
else : # Unless it's empty; then, well... just say no relays yet.
2015-07-18 07:00:25 +02:00
s + = ' (no relays yet) '
2015-11-29 06:09:16 +01:00
2015-09-07 07:23:44 +02:00
irc . msg ( source , s )
2015-08-12 13:18:20 +02:00
2016-05-01 01:54:11 +02:00
if irc . isOper ( source ) :
2016-01-10 05:30:54 +01:00
s = ' '
2015-11-29 06:09:16 +01:00
# If the caller is an oper, we can show the hostmasks of people
# that created all the available channels (Janus does this too!!)
2015-12-27 00:24:06 +01:00
creator = v . get ( ' creator ' )
if creator :
# But only if the value actually exists (old DBs will have it
# missing).
2016-01-10 05:30:54 +01:00
s + = ' by \x02 %s \x02 ' % creator
# Ditto for creation date
ts = v . get ( ' ts ' )
if ts :
s + = ' on %s ' % time . ctime ( ts )
2016-02-22 05:04:53 +01:00
if s : # Indent to make the list look nicer
2016-01-10 05:30:54 +01:00
irc . msg ( source , ' Channel created %s . ' % s )
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>
Allows blocking / unblocking certain networks from linking to a relay , based on a blacklist .
LINKACL LIST returns a list of blocked networks for a channel , while the ALLOW and DENY subcommands allow manipulating this blacklist . """
2015-08-26 05:55:39 +02:00
missingargs = " Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY). "
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-08-26 05:18:14 +02:00
try :
cmd = args [ 0 ] . lower ( )
2015-09-02 07:01:22 +02:00
channel = utils . toLower ( irc , args [ 1 ] )
2015-08-26 05:18:14 +02:00
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( missingargs )
2015-08-26 05:18:14 +02:00
return
if not utils . isChannel ( channel ) :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Invalid channel %r . ' % channel )
2015-08-26 05:18:14 +02:00
return
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , channel ) )
2015-08-26 05:18:14 +02:00
if not relay :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-08-26 05:18:14 +02:00
return
if cmd == ' list ' :
s = ' Blocked networks for \x02 %s \x02 : \x02 %s \x02 ' % ( channel , ' , ' . join ( db [ relay ] [ ' blocked_nets ' ] ) or ' (empty) ' )
2015-10-24 03:29:10 +02:00
irc . reply ( s )
2015-08-26 05:18:14 +02:00
return
try :
remotenet = args [ 2 ]
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( missingargs )
2015-08-26 05:18:14 +02:00
return
if cmd == ' deny ' :
db [ relay ] [ ' blocked_nets ' ] . add ( remotenet )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-08-26 05:18:14 +02:00
elif cmd == ' allow ' :
try :
db [ relay ] [ ' blocked_nets ' ] . remove ( remotenet )
except KeyError :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Network %r is not on the blacklist for %r . ' % ( remotenet , channel ) )
2015-08-26 05:18:14 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-08-26 05:18:14 +02:00
else :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: Unknown subcommand %r : valid ones are ALLOW, DENY, and LIST. ' % cmd )
2015-08-30 04:29:05 +02:00
@utils.add_cmd
def showuser ( irc , source , args ) :
""" <user>
2015-09-19 19:39:17 +02:00
Shows relay data about user < user > . This supplements the ' showuser ' command in the ' commands ' plugin , which provides more general information . """
2015-08-30 04:29:05 +02:00
try :
target = args [ 0 ]
except IndexError :
# No errors here; showuser from the commands plugin already does this
# for us.
return
2016-01-01 02:28:47 +01:00
u = irc . nickToUid ( target )
2015-08-30 04:49:37 +02:00
if u :
2015-08-30 04:29:05 +02:00
try :
2015-09-15 02:29:37 +02:00
userpair = getOrigUser ( irc , u ) or ( irc . name , u )
2015-08-30 04:29:05 +02:00
remoteusers = relayusers [ userpair ] . items ( )
except KeyError :
pass
else :
nicks = [ ]
if remoteusers :
2015-09-13 01:03:59 +02:00
nicks . append ( ' %s : \x02 %s \x02 ' % ( userpair [ 0 ] ,
2015-08-30 04:49:37 +02:00
world . networkobjects [ userpair [ 0 ] ] . users [ userpair [ 1 ] ] . nick ) )
2015-08-30 04:29:05 +02:00
for r in remoteusers :
remotenet , remoteuser = r
remoteirc = world . networkobjects [ remotenet ]
2015-09-13 01:03:59 +02:00
nicks . append ( ' %s : \x02 %s \x02 ' % ( remotenet , remoteirc . users [ remoteuser ] . nick ) )
2015-09-07 07:23:44 +02:00
irc . msg ( source , " \x02 Relay nicks \x02 : %s " % ' , ' . join ( nicks ) )
2015-08-30 04:29:05 +02:00
relaychannels = [ ]
for ch in irc . users [ u ] . channels :
2015-09-15 02:29:37 +02:00
relay = getRelay ( ( irc . name , ch ) )
2015-08-30 04:29:05 +02:00
if relay :
relaychannels . append ( ' ' . join ( relay ) )
2016-05-01 01:54:11 +02:00
if relaychannels and ( irc . isOper ( source ) or u == source ) :
2015-09-07 07:23:44 +02:00
irc . msg ( source , " \x02 Relay channels \x02 : %s " % ' ' . join ( relaychannels ) )
2015-09-15 02:23:56 +02:00
@utils.add_cmd
def save ( irc , source , args ) :
""" takes no arguments.
Saves the relay database to disk . """
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-09-18 04:22:34 +02:00
exportDB ( )
2015-10-24 03:29:10 +02:00
irc . reply ( ' Done. ' )
2015-09-17 05:59:32 +02:00
@utils.add_cmd
def claim ( irc , source , args ) :
""" <channel> [<comma separated list of networks>]
Sets the CLAIM for a channel to a case - sensitive list of networks . If no list of networks is given , shows which networks have claim over the channel . A single hyphen ( - ) can also be given as a list of networks to remove claim from the channel entirely .
CLAIM is a way of enforcing network ownership for a channel , similarly to Janus . Unless the list is empty , only networks on the CLAIM list for a channel ( plus the creating network ) are allowed to override kicks , mode changes , and topic changes in it - attempts from other networks ' opers to do this are simply blocked or reverted. " " "
2016-05-01 01:54:11 +02:00
irc . checkAuthenticated ( source )
2015-09-17 05:59:32 +02:00
try :
channel = utils . toLower ( irc , args [ 0 ] )
except IndexError :
2015-10-24 03:29:10 +02:00
irc . reply ( " Error: Not enough arguments. Needs 1-2: channel, list of networks (optional). " )
2015-09-17 05:59:32 +02:00
return
# We override getRelay() here to limit the search to the current network.
relay = ( irc . name , channel )
if relay not in db :
2015-10-24 03:29:10 +02:00
irc . reply ( ' Error: No such relay %r exists. ' % channel )
2015-09-17 05:59:32 +02:00
return
claimed = db [ relay ] [ " claim " ]
try :
nets = args [ 1 ] . strip ( )
except IndexError : # No networks given.
2015-10-24 03:29:10 +02:00
irc . reply ( ' Channel \x02 %s \x02 is claimed by: %s ' %
2015-09-26 18:39:45 +02:00
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )
2015-09-17 05:59:32 +02:00
else :
if nets == ' - ' or not nets :
claimed = set ( )
else :
claimed = set ( nets . split ( ' , ' ) )
db [ relay ] [ " claim " ] = claimed
2015-10-24 03:29:10 +02:00
irc . reply ( ' CLAIM for channel \x02 %s \x02 set to: %s ' %
2015-09-26 18:39:45 +02:00
( channel , ' , ' . join ( claimed ) or ' \x1D (none) \x1D ' ) )