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__))))
|
|
|
|
import pickle
|
|
|
|
import sched
|
|
|
|
import threading
|
|
|
|
import time
|
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
|
|
|
|
|
|
|
|
dbname = "pylinkrelay.db"
|
2015-07-13 08:28:54 +02:00
|
|
|
relayusers = defaultdict(dict)
|
2015-07-11 05:26:46 +02:00
|
|
|
|
2015-07-13 08:28:54 +02:00
|
|
|
def normalizeNick(irc, netname, nick, separator="/"):
|
2015-07-15 22:53:14 +02:00
|
|
|
# Block until we know the IRC network's nick length (after capabilities
|
|
|
|
# are sent)
|
2015-07-17 23:37:07 +02:00
|
|
|
log.debug('(%s) normalizeNick: waiting for irc.connected', irc.name)
|
2015-07-18 01:20:41 +02:00
|
|
|
irc.connected.wait(1)
|
2015-07-15 22:53:14 +02:00
|
|
|
|
2015-07-12 23:02:17 +02:00
|
|
|
orig_nick = nick
|
|
|
|
protoname = irc.proto.__name__
|
|
|
|
maxnicklen = irc.maxnicklen
|
|
|
|
if protoname == 'charybdis':
|
|
|
|
# Charybdis doesn't allow / in usernames, and will quit with
|
|
|
|
# a protocol violation if there is one.
|
|
|
|
separator = separator.replace('/', '|')
|
|
|
|
nick = nick.replace('/', '|')
|
2015-07-13 01:59:49 +02:00
|
|
|
if nick.startswith(tuple(string.digits)):
|
|
|
|
# On TS6 IRCd-s, nicks that start with 0-9 are only allowed if
|
|
|
|
# they match the UID of the originating server. Otherwise, you'll
|
|
|
|
# get nasty protocol violations!
|
|
|
|
nick = '_' + nick
|
2015-07-12 23:02:17 +02:00
|
|
|
tagnicks = True
|
|
|
|
|
|
|
|
suffix = separator + netname
|
|
|
|
nick = nick[:maxnicklen]
|
|
|
|
# Maximum allowed length of a nickname.
|
|
|
|
allowedlength = maxnicklen - len(suffix)
|
|
|
|
# If a nick is too long, the real nick portion must be cut off, but the
|
|
|
|
# /network suffix must remain the same.
|
|
|
|
|
|
|
|
nick = nick[:allowedlength]
|
|
|
|
nick += suffix
|
2015-07-15 22:53:14 +02:00
|
|
|
# TODO: factorize
|
|
|
|
while utils.nickToUid(irc, nick) and not utils.isInternalClient(irc, utils.nickToUid(irc, nick)):
|
|
|
|
# The nick we want exists? Darn, create another one then, but only if
|
|
|
|
# the target isn't an internal client!
|
2015-07-12 23:02:17 +02:00
|
|
|
# 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.
|
|
|
|
new_sep = separator + separator[-1]
|
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 " \
|
|
|
|
"nick length (got: %s, allowed: %s!" % (nick, finalLength, maxnicklen)
|
|
|
|
|
|
|
|
return nick
|
|
|
|
|
2015-07-11 05:26:46 +02:00
|
|
|
def loadDB():
|
|
|
|
global db
|
|
|
|
try:
|
|
|
|
with open(dbname, "rb") as f:
|
|
|
|
db = pickle.load(f)
|
|
|
|
except (ValueError, IOError):
|
|
|
|
log.exception("Relay: failed to load links database %s"
|
|
|
|
", creating a new one in memory...", dbname)
|
|
|
|
db = {}
|
|
|
|
|
2015-07-17 22:39:57 +02:00
|
|
|
def exportDB():
|
|
|
|
scheduler = utils.schedulers.get('relaydb')
|
|
|
|
if scheduler:
|
|
|
|
scheduler.enter(30, 1, exportDB)
|
2015-07-12 22:09:35 +02:00
|
|
|
log.debug("Relay: exporting links database to %s", dbname)
|
2015-07-11 05:26:46 +02:00
|
|
|
with open(dbname, 'wb') as f:
|
|
|
|
pickle.dump(db, f, protocol=4)
|
|
|
|
|
2015-07-14 06:46:05 +02:00
|
|
|
def getPrefixModes(irc, remoteirc, channel, user):
|
|
|
|
modes = ''
|
|
|
|
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
|
|
|
|
if pmode not in remoteirc.cmodes: # Mode not supported by IRCd
|
|
|
|
continue
|
|
|
|
if user in irc.channels[channel].prefixmodes[pmode+'s']:
|
|
|
|
modes += remoteirc.cmodes[pmode]
|
|
|
|
return modes
|
|
|
|
|
2015-07-14 21:04:05 +02:00
|
|
|
def getRemoteUser(irc, remoteirc, user):
|
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 == remoteirc.pseudoclient.uid:
|
|
|
|
return irc.pseudoclient.uid
|
|
|
|
if user == irc.pseudoclient.uid:
|
|
|
|
return remoteirc.pseudoclient.uid
|
|
|
|
except AttributeError: # Network hasn't been initialized yet?
|
|
|
|
pass
|
2015-07-14 21:04:05 +02:00
|
|
|
try:
|
|
|
|
u = relayusers[(irc.name, user)][remoteirc.name]
|
|
|
|
except KeyError:
|
2015-07-15 07:37:50 +02:00
|
|
|
userobj = irc.users.get(user)
|
2015-07-15 20:48:03 +02:00
|
|
|
if userobj is None or not remoteirc.connected:
|
|
|
|
# The query wasn't actually a valid user, or the network hasn't
|
|
|
|
# been connected yet... Oh well!
|
2015-07-15 07:37:50 +02:00
|
|
|
return
|
2015-07-14 21:04:05 +02:00
|
|
|
nick = normalizeNick(remoteirc, irc.name, userobj.nick)
|
|
|
|
ident = userobj.ident
|
|
|
|
host = userobj.host
|
|
|
|
realname = userobj.realname
|
|
|
|
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
|
2015-07-15 02:42:17 +02:00
|
|
|
host=host, realname=realname).uid
|
2015-07-14 21:04:05 +02:00
|
|
|
remoteirc.users[u].remote = irc.name
|
|
|
|
relayusers[(irc.name, user)][remoteirc.name] = u
|
|
|
|
remoteirc.users[u].remote = irc.name
|
|
|
|
return u
|
|
|
|
|
2015-07-15 04:39:49 +02:00
|
|
|
def getLocalUser(irc, user):
|
|
|
|
# Our target is an internal client, which means someone
|
|
|
|
# is kicking a remote user over the relay.
|
|
|
|
# We have to find the real target for the KICK. This is like
|
|
|
|
# findRemoteUser, but in reverse.
|
|
|
|
# First, iterate over everyone!
|
|
|
|
for k, v in relayusers.items():
|
|
|
|
log.debug('(%s) getLocalUser: processing %s, %s in relayusers', irc.name, k, v)
|
|
|
|
if k[0] == irc.name:
|
|
|
|
# We don't need to do anything if the target users is on
|
|
|
|
# the same network as us.
|
|
|
|
log.debug('(%s) getLocalUser: skipping %s since the target network matches the source network.', irc.name, k)
|
|
|
|
continue
|
2015-07-15 07:37:50 +02:00
|
|
|
if v.get(irc.name) == user:
|
2015-07-15 04:39:49 +02:00
|
|
|
# If the stored pseudoclient UID for the kicked user on
|
|
|
|
# this network matches the target we have, set that user
|
|
|
|
# as the one we're kicking! It's a handful, but remember
|
|
|
|
# we're mapping (home network, UID) pairs to their
|
|
|
|
# respective relay pseudoclients on other networks.
|
|
|
|
remoteuser = k
|
|
|
|
log.debug('(%s) getLocalUser: found %s to correspond to %s.', irc.name, v, k)
|
|
|
|
return remoteuser
|
|
|
|
|
2015-07-13 08:28:54 +02:00
|
|
|
def findRelay(chanpair):
|
|
|
|
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-07-14 21:04:05 +02:00
|
|
|
def findRemoteChan(irc, remoteirc, channel):
|
|
|
|
query = (irc.name, channel)
|
|
|
|
remotenetname = remoteirc.name
|
2015-07-14 04:46:24 +02:00
|
|
|
chanpair = findRelay(query)
|
|
|
|
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-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-07-13 09:01:04 +02:00
|
|
|
c = irc.channels[channel]
|
|
|
|
relay = findRelay((irc.name, channel))
|
2015-07-14 06:46:05 +02:00
|
|
|
log.debug('(%s) initializeChannel being called on %s', irc.name, channel)
|
|
|
|
log.debug('(%s) 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:
|
2015-07-18 06:51:30 +02:00
|
|
|
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel)
|
2015-07-14 06:46:05 +02:00
|
|
|
all_links = db[relay]['links'].copy()
|
|
|
|
all_links.update((relay,))
|
|
|
|
log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links)
|
|
|
|
for link in all_links:
|
2015-07-14 07:42:50 +02:00
|
|
|
modes = []
|
2015-07-13 09:01:04 +02:00
|
|
|
remotenet, remotechan = link
|
2015-07-14 06:46:05 +02:00
|
|
|
if remotenet == irc.name:
|
|
|
|
continue
|
|
|
|
remoteirc = utils.networkobjects[remotenet]
|
|
|
|
rc = remoteirc.channels[remotechan]
|
2015-07-16 21:25:09 +02:00
|
|
|
if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)):
|
|
|
|
continue # They aren't connected, don't bother!
|
2015-07-14 06:46:05 +02:00
|
|
|
for user in remoteirc.channels[remotechan].users:
|
2015-07-14 21:04:05 +02:00
|
|
|
# Don't spawn our pseudoclients again.
|
2015-07-14 06:46:05 +02:00
|
|
|
if not utils.isInternalClient(remoteirc, user):
|
|
|
|
log.debug('(%s) initializeChannel: should be joining %s/%s to %s', irc.name, user, remotenet, channel)
|
2015-07-14 21:04:05 +02:00
|
|
|
remoteuser = getRemoteUser(remoteirc, irc, user)
|
|
|
|
userpair = (getPrefixModes(remoteirc, irc, remotechan, user), remoteuser)
|
|
|
|
log.debug('(%s) initializeChannel: adding %s to queued_users for %s', irc.name, userpair, channel)
|
|
|
|
queued_users.append(userpair)
|
|
|
|
if queued_users:
|
2015-07-15 02:42:17 +02:00
|
|
|
irc.proto.sjoinServer(irc, irc.sid, channel, queued_users, ts=rc.ts)
|
2015-07-15 04:39:49 +02:00
|
|
|
relayModes(remoteirc, irc, remoteirc.sid, remotechan)
|
|
|
|
relayModes(irc, remoteirc, irc.sid, channel)
|
2015-07-18 06:51:30 +02:00
|
|
|
topic = remoteirc.channels[relay[1]].topic
|
|
|
|
# XXX: find a more elegant way to do this
|
|
|
|
# Only update the topic if it's different from what we already have.
|
|
|
|
if topic and topic != irc.channels[channel].topic:
|
|
|
|
irc.proto.topicServer(irc, irc.sid, channel, topic)
|
2015-07-14 21:04:05 +02:00
|
|
|
|
|
|
|
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
|
2015-07-14 06:46:05 +02:00
|
|
|
relayJoins(irc, channel, c.users, c.ts, c.modes)
|
2015-07-13 08:28:54 +02:00
|
|
|
|
|
|
|
def handle_join(irc, numeric, command, args):
|
|
|
|
channel = args['channel']
|
|
|
|
if not findRelay((irc.name, channel)):
|
|
|
|
# No relay here, return.
|
|
|
|
return
|
|
|
|
modes = args['modes']
|
|
|
|
ts = args['ts']
|
|
|
|
users = set(args['users'])
|
2015-07-13 09:01:04 +02:00
|
|
|
# users.update(irc.channels[channel].users)
|
|
|
|
relayJoins(irc, channel, users, ts, modes)
|
|
|
|
utils.add_hook(handle_join, 'JOIN')
|
|
|
|
|
2015-07-14 04:53:57 +02:00
|
|
|
def handle_quit(irc, numeric, command, args):
|
|
|
|
ouruser = numeric
|
|
|
|
for netname, user in relayusers[(irc.name, numeric)].items():
|
|
|
|
remoteirc = utils.networkobjects[netname]
|
|
|
|
remoteirc.proto.quitClient(remoteirc, user, args['text'])
|
|
|
|
del relayusers[(irc.name, ouruser)]
|
|
|
|
utils.add_hook(handle_quit, 'QUIT')
|
|
|
|
|
2015-07-16 04:09:10 +02:00
|
|
|
def handle_squit(irc, numeric, command, args):
|
|
|
|
users = args['users']
|
|
|
|
for user in users:
|
|
|
|
log.debug('(%s) relay handle_squit: sending handle_quit on %s', irc.name, user)
|
|
|
|
handle_quit(irc, user, command, {'text': '*.net *.split'})
|
|
|
|
utils.add_hook(handle_squit, 'SQUIT')
|
|
|
|
|
2015-07-14 04:53:57 +02:00
|
|
|
def handle_nick(irc, numeric, command, args):
|
|
|
|
for netname, user in relayusers[(irc.name, numeric)].items():
|
|
|
|
remoteirc = utils.networkobjects[netname]
|
|
|
|
newnick = normalizeNick(remoteirc, irc.name, args['newnick'])
|
2015-07-15 20:48:26 +02:00
|
|
|
if remoteirc.users[user].nick != newnick:
|
|
|
|
remoteirc.proto.nickClient(remoteirc, user, newnick)
|
2015-07-14 04:53:57 +02:00
|
|
|
utils.add_hook(handle_nick, 'NICK')
|
|
|
|
|
|
|
|
def handle_part(irc, numeric, command, args):
|
|
|
|
channel = args['channel']
|
|
|
|
text = args['text']
|
2015-07-16 21:25:09 +02:00
|
|
|
for netname, user in relayusers.copy()[(irc.name, numeric)].items():
|
2015-07-14 04:53:57 +02:00
|
|
|
remoteirc = utils.networkobjects[netname]
|
2015-07-14 21:04:05 +02:00
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
2015-07-14 04:53:57 +02:00
|
|
|
remoteirc.proto.partClient(remoteirc, user, remotechan, text)
|
2015-07-16 21:25:09 +02:00
|
|
|
if not remoteirc.users[user].channels:
|
|
|
|
remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.')
|
|
|
|
del relayusers[(irc.name, numeric)][remoteirc.name]
|
2015-07-14 04:53:57 +02:00
|
|
|
utils.add_hook(handle_part, 'PART')
|
|
|
|
|
2015-07-15 04:39:49 +02:00
|
|
|
def handle_privmsg(irc, numeric, command, args):
|
|
|
|
notice = (command == 'NOTICE')
|
|
|
|
target = args['target']
|
|
|
|
text = args['text']
|
|
|
|
for netname, user in relayusers[(irc.name, numeric)].items():
|
|
|
|
remoteirc = utils.networkobjects[netname]
|
|
|
|
if utils.isChannel(target):
|
|
|
|
real_target = findRemoteChan(irc, remoteirc, target)
|
|
|
|
if not real_target:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
real_target = getLocalUser(irc, target)[1]
|
|
|
|
except TypeError:
|
|
|
|
real_target = getRemoteUser(irc, remoteirc, target)
|
|
|
|
if notice:
|
|
|
|
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
|
|
|
|
else:
|
|
|
|
remoteirc.proto.messageClient(remoteirc, user, real_target, text)
|
|
|
|
utils.add_hook(handle_privmsg, 'PRIVMSG')
|
|
|
|
utils.add_hook(handle_privmsg, 'NOTICE')
|
|
|
|
|
2015-07-15 03:20:20 +02:00
|
|
|
def handle_kick(irc, source, command, args):
|
|
|
|
channel = args['channel']
|
|
|
|
target = args['target']
|
|
|
|
text = args['text']
|
|
|
|
kicker = source
|
|
|
|
kicker_modes = getPrefixModes(irc, irc, channel, kicker)
|
|
|
|
relay = findRelay((irc.name, channel))
|
|
|
|
if relay is None:
|
|
|
|
return
|
|
|
|
for name, remoteirc in utils.networkobjects.items():
|
|
|
|
if irc.name == name:
|
|
|
|
continue
|
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
|
|
|
log.debug('(%s) Relay kick: remotechan for %s on %s is %s', irc.name, channel, name, remotechan)
|
|
|
|
if remotechan is None:
|
|
|
|
continue
|
|
|
|
real_kicker = getRemoteUser(irc, remoteirc, kicker)
|
|
|
|
log.debug('(%s) Relay kick: real kicker for %s on %s is %s', irc.name, kicker, name, real_kicker)
|
2015-07-15 07:37:50 +02:00
|
|
|
if real_kicker is None or not utils.isInternalClient(irc, target):
|
2015-07-15 03:20:20 +02:00
|
|
|
log.debug('(%s) Relay kick: target %s is NOT an internal client', irc.name, target)
|
|
|
|
# Both the target and kicker are external clients; i.e.
|
|
|
|
# they originate from the same network. We shouldn't have
|
|
|
|
# to process this any further, because the uplink IRCd
|
|
|
|
# will handle this appropriately, and we'll just follow.
|
|
|
|
real_target = getRemoteUser(irc, remoteirc, target)
|
|
|
|
log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target)
|
2015-07-15 07:37:50 +02:00
|
|
|
if real_kicker:
|
|
|
|
remoteirc.proto.kickClient(remoteirc, real_kicker,
|
|
|
|
remotechan, real_target, text)
|
|
|
|
else: # Kick originated from a server, not a client.
|
|
|
|
try:
|
|
|
|
text = "(%s@%s) %s" % (irc.servers[kicker].name, irc.name, text)
|
|
|
|
except (KeyError, AttributeError):
|
|
|
|
text = "(<unknown server>@%s) %s" % (irc.name, text)
|
|
|
|
remoteirc.proto.kickServer(remoteirc, remoteirc.sid,
|
|
|
|
remotechan, real_target, text)
|
2015-07-15 03:20:20 +02:00
|
|
|
else:
|
|
|
|
log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target)
|
2015-07-15 04:39:49 +02:00
|
|
|
real_target = getLocalUser(irc, target)[1]
|
2015-07-15 03:20:20 +02:00
|
|
|
log.debug('(%s) Relay kick: kicker_modes are %r', irc.name, kicker_modes)
|
2015-07-15 03:27:26 +02:00
|
|
|
if irc.name not in db[relay]['claim'] and not \
|
2015-07-15 03:20:20 +02:00
|
|
|
any([mode in kicker_modes for mode in ('q', 'a', 'o', 'h')]):
|
|
|
|
log.debug('(%s) Relay kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
|
|
|
|
# Home network is not in the channel's claim AND the kicker is not
|
|
|
|
# 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.
|
|
|
|
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, target)])
|
|
|
|
utils.msg(irc, kicker, "This channel is claimed; your kick has "
|
|
|
|
"to %s been blocked because you are not "
|
|
|
|
"(half)opped." % channel, notice=True)
|
|
|
|
else:
|
|
|
|
# Propogate the kick!
|
|
|
|
log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan, real_kicker, kicker, irc.name)
|
|
|
|
remoteirc.proto.kickClient(remoteirc, real_kicker,
|
2015-07-15 04:39:49 +02:00
|
|
|
remotechan, real_target, args['text'])
|
2015-07-15 03:20:20 +02:00
|
|
|
utils.add_hook(handle_kick, 'KICK')
|
|
|
|
|
2015-07-16 20:53:40 +02:00
|
|
|
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 = utils.networkobjects[netname]
|
|
|
|
try:
|
|
|
|
remoteirc.proto.updateClient(remoteirc, user, field, text)
|
|
|
|
except ValueError: # IRCd doesn't support changing the field we want
|
|
|
|
logging.debug('(%s) Error raised changing field %r of %s on %s (for %s/%s)', irc.name, field, user, target, remotenet, irc.name)
|
|
|
|
continue
|
|
|
|
|
|
|
|
for c in ('CHGHOST', 'CHGNAME', 'CHGIDENT'):
|
|
|
|
utils.add_hook(handle_chgclient, c)
|
|
|
|
|
2015-07-15 04:39:49 +02:00
|
|
|
def relayModes(irc, remoteirc, sender, channel, modes=None):
|
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
|
|
|
log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan)
|
|
|
|
if remotechan is None:
|
|
|
|
return
|
|
|
|
if modes is None:
|
2015-07-18 21:05:24 +02:00
|
|
|
modes = irc.channels[channel].modes
|
2015-07-15 04:39:49 +02:00
|
|
|
log.debug('(%s) Relay mode: channel data for %s%s: %s', irc.name, remoteirc.name, remotechan, remoteirc.channels[remotechan])
|
|
|
|
supported_modes = []
|
|
|
|
log.debug('(%s) Relay mode: initial modelist for %s is %s', irc.name, channel, modes)
|
|
|
|
for modepair in modes:
|
|
|
|
try:
|
|
|
|
prefix, modechar = modepair[0]
|
|
|
|
except ValueError:
|
|
|
|
modechar = modepair[0]
|
|
|
|
prefix = '+'
|
|
|
|
arg = modepair[1]
|
|
|
|
# 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:
|
|
|
|
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.
|
|
|
|
try:
|
|
|
|
arg = getLocalUser(irc, arg)[1]
|
|
|
|
except TypeError:
|
|
|
|
# getLocalUser returns None, raises None when trying to
|
|
|
|
# get [1] from it.
|
|
|
|
arg = getRemoteUser(irc, remoteirc, arg)
|
|
|
|
supported_char = remoteirc.cmodes.get(name)
|
|
|
|
if supported_char:
|
|
|
|
supported_modes.append((prefix+supported_char, arg))
|
|
|
|
log.debug('(%s) Relay mode: 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.
|
|
|
|
if sender in irc.users:
|
|
|
|
u = getRemoteUser(irc, remoteirc, sender)
|
2015-07-18 21:05:24 +02:00
|
|
|
remoteirc.proto.modeClient(remoteirc, u, remotechan, supported_modes)
|
2015-07-17 01:27:17 +02:00
|
|
|
else:
|
2015-07-18 21:05:24 +02:00
|
|
|
remoteirc.proto.modeServer(remoteirc, remoteirc.sid, remotechan, supported_modes)
|
2015-07-15 04:39:49 +02:00
|
|
|
|
|
|
|
def handle_mode(irc, numeric, command, args):
|
|
|
|
target = args['target']
|
|
|
|
if not utils.isChannel(target):
|
|
|
|
### TODO: handle user mode changes too
|
|
|
|
return
|
|
|
|
modes = args['modes']
|
|
|
|
for name, remoteirc in utils.networkobjects.items():
|
|
|
|
if irc.name == name:
|
|
|
|
continue
|
|
|
|
relayModes(irc, remoteirc, numeric, target, modes)
|
|
|
|
utils.add_hook(handle_mode, 'MODE')
|
|
|
|
|
2015-07-15 08:24:21 +02:00
|
|
|
def handle_topic(irc, numeric, command, args):
|
|
|
|
channel = args['channel']
|
|
|
|
topic = args['topic']
|
|
|
|
# XXX: find a more elegant way to do this
|
|
|
|
# Topics with content take precedence over empty topics.
|
|
|
|
# This prevents us from overwriting topics on channels with
|
|
|
|
# emptiness just because a leaf network hasn't received it yet.
|
|
|
|
if topic:
|
|
|
|
for name, remoteirc in utils.networkobjects.items():
|
|
|
|
if irc.name == name:
|
|
|
|
continue
|
|
|
|
|
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
2015-07-15 20:48:26 +02:00
|
|
|
# Don't send if the remote topic is the same as ours.
|
|
|
|
if remotechan is None or topic == remoteirc.channels[remotechan].topic:
|
2015-07-15 08:24:21 +02:00
|
|
|
continue
|
|
|
|
# This might originate from a server too.
|
|
|
|
remoteuser = getRemoteUser(irc, remoteirc, numeric)
|
|
|
|
if remoteuser:
|
|
|
|
remoteirc.proto.topicClient(remoteirc, remoteuser, remotechan, topic)
|
|
|
|
else:
|
|
|
|
remoteirc.proto.topicServer(remoteirc, remoteirc.sid, remotechan, topic)
|
|
|
|
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']
|
|
|
|
# We don't allow killing over the relay, so we must spawn the client.
|
|
|
|
# all over again and rejoin it to its channels.
|
|
|
|
realuser = getLocalUser(irc, target)
|
|
|
|
del relayusers[realuser][irc.name]
|
|
|
|
remoteirc = utils.networkobjects[realuser[0]]
|
|
|
|
for channel in remoteirc.channels:
|
|
|
|
remotechan = findRemoteChan(remoteirc, irc, channel)
|
|
|
|
if remotechan:
|
|
|
|
modes = getPrefixModes(remoteirc, irc, remotechan, realuser[1])
|
|
|
|
log.debug('(%s) handle_kill: userpair: %s, %s', irc.name, modes, realuser)
|
|
|
|
client = getRemoteUser(remoteirc, irc, realuser[1])
|
|
|
|
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)])
|
|
|
|
utils.msg(irc, numeric, "Your kill has to %s been blocked "
|
|
|
|
"because PyLink does not allow killing"
|
|
|
|
" users over the relay at this time." % \
|
|
|
|
userdata.nick, notice=True)
|
|
|
|
utils.add_hook(handle_kill, 'KILL')
|
|
|
|
|
2015-07-13 09:01:04 +02:00
|
|
|
def relayJoins(irc, channel, users, ts, modes):
|
2015-07-14 04:53:57 +02:00
|
|
|
queued_users = []
|
2015-07-15 03:27:26 +02:00
|
|
|
for user in users.copy():
|
2015-07-13 08:28:54 +02:00
|
|
|
try:
|
|
|
|
if irc.users[user].remote:
|
2015-07-14 06:46:05 +02:00
|
|
|
# Is the .remote attribute set? If so, don't relay already
|
2015-07-13 08:28:54 +02:00
|
|
|
# relayed clients; that'll trigger an endless loop!
|
|
|
|
continue
|
|
|
|
except AttributeError: # Nope, it isn't.
|
|
|
|
pass
|
|
|
|
if user == irc.pseudoclient.uid:
|
|
|
|
# We don't need to clone the PyLink pseudoclient... That's
|
|
|
|
# meaningless.
|
|
|
|
continue
|
|
|
|
log.debug('Okay, spawning %s/%s everywhere', user, irc.name)
|
|
|
|
for name, remoteirc in utils.networkobjects.items():
|
|
|
|
if name == irc.name:
|
|
|
|
# Don't relay things to their source network...
|
|
|
|
continue
|
2015-07-14 21:04:05 +02:00
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
2015-07-16 21:25:09 +02:00
|
|
|
if remotechan is None:
|
|
|
|
# If there is no link on our network for the user, don't
|
|
|
|
# bother spawning it.
|
|
|
|
continue
|
|
|
|
u = getRemoteUser(irc, remoteirc, user)
|
|
|
|
if u is None:
|
2015-07-14 06:46:05 +02:00
|
|
|
continue
|
2015-07-14 21:04:05 +02:00
|
|
|
ts = irc.channels[channel].ts
|
|
|
|
# TODO: join users in batches with SJOIN, not one by one.
|
|
|
|
prefixes = getPrefixModes(irc, remoteirc, channel, user)
|
|
|
|
userpair = (prefixes, u)
|
|
|
|
log.debug('(%s) relayJoin: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan)
|
2015-07-15 02:42:17 +02:00
|
|
|
remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, [userpair], ts=ts)
|
2015-07-15 04:39:49 +02:00
|
|
|
relayModes(irc, remoteirc, irc.sid, channel, modes)
|
2015-07-13 08:28:54 +02:00
|
|
|
|
2015-07-14 08:29:20 +02:00
|
|
|
def relayPart(irc, channel, user):
|
|
|
|
for name, remoteirc in utils.networkobjects.items():
|
|
|
|
if name == irc.name:
|
|
|
|
# Don't relay things to their source network...
|
|
|
|
continue
|
2015-07-14 21:04:05 +02:00
|
|
|
remotechan = findRemoteChan(irc, remoteirc, channel)
|
2015-07-14 08:29:20 +02:00
|
|
|
log.debug('(%s) relayPart: looking for %s/%s on %s', irc.name, user, irc.name, remoteirc.name)
|
|
|
|
log.debug('(%s) relayPart: remotechan found as %s', irc.name, remotechan)
|
2015-07-14 21:04:05 +02:00
|
|
|
remoteuser = getRemoteUser(irc, remoteirc, user)
|
2015-07-14 08:29:20 +02:00
|
|
|
log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser)
|
|
|
|
if remotechan is None:
|
|
|
|
continue
|
|
|
|
remoteirc.proto.partClient(remoteirc, remoteuser, remotechan, 'Channel delinked.')
|
2015-07-16 21:25:09 +02:00
|
|
|
if not remoteirc.users[remoteuser].channels:
|
|
|
|
remoteirc.proto.quitClient(remoteirc, remoteuser, 'Left all shared channels.')
|
|
|
|
del relayusers[(irc.name, user)][remoteirc.name]
|
2015-07-14 08:29:20 +02:00
|
|
|
|
2015-07-13 08:28:54 +02:00
|
|
|
def removeChannel(irc, channel):
|
2015-07-13 04:03:18 +02:00
|
|
|
if channel not in map(str.lower, irc.serverdata['channels']):
|
|
|
|
irc.proto.partClient(irc, irc.pseudoclient.uid, channel)
|
2015-07-14 08:29:20 +02:00
|
|
|
relay = findRelay((irc.name, channel))
|
|
|
|
if relay:
|
|
|
|
all_links = db[relay]['links'].copy()
|
|
|
|
all_links.update((relay,))
|
|
|
|
log.debug('(%s) removeChannel: all_links: %s', irc.name, all_links)
|
|
|
|
for user in irc.channels[channel].users:
|
|
|
|
if not utils.isInternalClient(irc, user):
|
|
|
|
relayPart(irc, channel, user)
|
|
|
|
for link in all_links:
|
|
|
|
if link[0] == irc.name:
|
|
|
|
# Don't relay things to their source network...
|
|
|
|
continue
|
|
|
|
remotenet, remotechan = link
|
|
|
|
remoteirc = utils.networkobjects[remotenet]
|
|
|
|
rc = remoteirc.channels[remotechan]
|
|
|
|
for user in remoteirc.channels[remotechan].users.copy():
|
|
|
|
log.debug('(%s) removeChannel: part user %s/%s from %s', irc.name, user, remotenet, remotechan)
|
|
|
|
if not utils.isInternalClient(remoteirc, user):
|
|
|
|
relayPart(remoteirc, remotechan, user)
|
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:
|
|
|
|
channel = args[0].lower()
|
|
|
|
except IndexError:
|
|
|
|
utils.msg(irc, source, "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):
|
|
|
|
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
|
|
|
return
|
2015-07-13 02:59:09 +02:00
|
|
|
if source not in irc.channels[channel].users:
|
2015-07-11 05:26:46 +02:00
|
|
|
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % channel)
|
|
|
|
return
|
|
|
|
if not utils.isOper(irc, source):
|
2015-07-13 02:59:09 +02:00
|
|
|
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
2015-07-11 05:26:46 +02:00
|
|
|
return
|
2015-07-13 02:59:09 +02:00
|
|
|
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
|
2015-07-13 08:28:54 +02:00
|
|
|
initializeChannel(irc, channel)
|
2015-07-11 05:26:46 +02:00
|
|
|
utils.msg(irc, source, 'Done.')
|
|
|
|
|
|
|
|
@utils.add_cmd
|
|
|
|
def destroy(irc, source, args):
|
|
|
|
"""<channel>
|
|
|
|
|
2015-07-18 07:35:34 +02:00
|
|
|
Removes <channel> from the relay, delinking all networks linked to it."""
|
2015-07-11 05:26:46 +02:00
|
|
|
try:
|
|
|
|
channel = args[0].lower()
|
|
|
|
except IndexError:
|
|
|
|
utils.msg(irc, source, "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):
|
|
|
|
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
|
|
|
return
|
|
|
|
if not utils.isOper(irc, source):
|
2015-07-13 02:59:09 +02:00
|
|
|
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
2015-07-11 05:26:46 +02:00
|
|
|
return
|
|
|
|
|
2015-07-14 08:29:20 +02:00
|
|
|
entry = (irc.name, channel)
|
|
|
|
if entry in db:
|
|
|
|
for link in db[entry]['links']:
|
|
|
|
removeChannel(utils.networkobjects[link[0]], link[1])
|
2015-07-13 08:28:54 +02:00
|
|
|
removeChannel(irc, channel)
|
2015-07-14 08:29:20 +02:00
|
|
|
del db[entry]
|
2015-07-11 05:26:46 +02:00
|
|
|
utils.msg(irc, source, 'Done.')
|
|
|
|
else:
|
|
|
|
utils.msg(irc, source, '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:
|
|
|
|
channel = args[1].lower()
|
|
|
|
remotenet = args[0].lower()
|
|
|
|
except IndexError:
|
|
|
|
utils.msg(irc, source, "Error: not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
localchan = args[2].lower()
|
|
|
|
except IndexError:
|
|
|
|
localchan = channel
|
|
|
|
for c in (channel, localchan):
|
|
|
|
if not utils.isChannel(c):
|
|
|
|
utils.msg(irc, source, 'Error: invalid channel %r.' % c)
|
|
|
|
return
|
|
|
|
if source not in irc.channels[localchan].users:
|
|
|
|
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % localchan)
|
|
|
|
return
|
|
|
|
if not utils.isOper(irc, source):
|
|
|
|
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
|
|
|
return
|
|
|
|
if remotenet not in utils.networkobjects:
|
|
|
|
utils.msg(irc, source, 'Error: no network named %r exists.' % remotenet)
|
|
|
|
return
|
2015-07-14 07:54:51 +02:00
|
|
|
localentry = findRelay((irc.name, localchan))
|
|
|
|
if localentry:
|
2015-07-13 02:59:09 +02:00
|
|
|
utils.msg(irc, source, 'Error: channel %r is already part of a relay.' % localchan)
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
entry = db[(remotenet, channel)]
|
|
|
|
except KeyError:
|
|
|
|
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
|
|
|
|
return
|
|
|
|
else:
|
2015-07-14 07:54:51 +02:00
|
|
|
for link in entry['links']:
|
|
|
|
if link[0] == irc.name:
|
|
|
|
utils.msg(irc, source, "Error: remote channel '%s%s' is already"
|
|
|
|
" linked here as %r." % (remotenet,
|
|
|
|
channel, link[1]))
|
|
|
|
return
|
2015-07-13 02:59:09 +02:00
|
|
|
entry['links'].add((irc.name, localchan))
|
2015-07-13 08:28:54 +02:00
|
|
|
initializeChannel(irc, localchan)
|
2015-07-13 02:59:09 +02:00
|
|
|
utils.msg(irc, source, '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:
|
|
|
|
channel = args[0].lower()
|
|
|
|
except IndexError:
|
|
|
|
utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: channel, remote netname (optional).")
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
remotenet = args[1].lower()
|
|
|
|
except IndexError:
|
|
|
|
remotenet = None
|
|
|
|
if not utils.isOper(irc, source):
|
|
|
|
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
|
|
|
return
|
|
|
|
if not utils.isChannel(channel):
|
|
|
|
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
|
|
|
return
|
2015-07-14 07:54:51 +02:00
|
|
|
entry = findRelay((irc.name, channel))
|
|
|
|
if entry:
|
|
|
|
if entry[0] == irc.name: # We own this channel.
|
|
|
|
if remotenet is None:
|
|
|
|
utils.msg(irc, source, "Error: you must select a network to delink, or use the 'destroy' command no remove this relay entirely.")
|
|
|
|
return
|
|
|
|
else:
|
2015-07-15 20:47:06 +02:00
|
|
|
for link in db[entry]['links'].copy():
|
|
|
|
if link[0] == remotenet:
|
|
|
|
removeChannel(utils.networkobjects[remotenet], link[1])
|
|
|
|
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-07-14 07:54:51 +02:00
|
|
|
utils.msg(irc, source, 'Done.')
|
2015-07-13 02:59:09 +02:00
|
|
|
else:
|
2015-07-14 07:54:51 +02:00
|
|
|
utils.msg(irc, source, 'Error: no such relay %r.' % channel)
|
2015-07-13 02:59:09 +02:00
|
|
|
|
2015-07-13 09:01:04 +02:00
|
|
|
def initializeAll(irc):
|
2015-07-17 23:37:07 +02:00
|
|
|
log.debug('(%s) initializeAll: waiting for utils.started', irc.name)
|
2015-07-14 01:07:55 +02:00
|
|
|
utils.started.wait()
|
2015-07-13 09:01:04 +02:00
|
|
|
for chanpair, entrydata in db.items():
|
|
|
|
network, channel = chanpair
|
|
|
|
initializeChannel(irc, channel)
|
|
|
|
for link in entrydata['links']:
|
|
|
|
network, channel = link
|
|
|
|
initializeChannel(irc, channel)
|
2015-07-18 00:08:24 +02:00
|
|
|
|
2015-07-13 08:28:54 +02:00
|
|
|
def main():
|
2015-07-11 05:26:46 +02:00
|
|
|
loadDB()
|
2015-07-13 08:28:54 +02:00
|
|
|
utils.schedulers['relaydb'] = scheduler = sched.scheduler()
|
2015-07-17 22:39:57 +02:00
|
|
|
scheduler.enter(30, 1, exportDB)
|
2015-07-13 08:28:54 +02:00
|
|
|
# Thread this because exportDB() queues itself as part of its
|
|
|
|
# execution, in order to get a repeating loop.
|
|
|
|
thread = threading.Thread(target=scheduler.run)
|
|
|
|
thread.daemon = True
|
|
|
|
thread.start()
|
2015-07-18 00:08:24 +02:00
|
|
|
|
|
|
|
def handle_endburst(irc, numeric, command, args):
|
|
|
|
initializeAll(irc)
|
|
|
|
utils.add_hook(handle_endburst, "ENDBURST")
|
2015-07-18 01:55:44 +02:00
|
|
|
|
|
|
|
def handle_disconnect(irc, numeric, command, args):
|
|
|
|
for k, v in relayusers.copy().items():
|
|
|
|
if irc.name in v:
|
|
|
|
del relayusers[k][irc.name]
|
|
|
|
if k[0] == irc.name:
|
|
|
|
handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'})
|
|
|
|
|
|
|
|
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
|
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."""
|
2015-07-18 07:00:25 +02:00
|
|
|
networks = list(utils.networkobjects.keys())
|
|
|
|
networks.remove(irc.name)
|
|
|
|
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks))
|
|
|
|
utils.msg(irc, source, s)
|
|
|
|
# Sort relay DB by channel name, and then sort.
|
|
|
|
for k, v in sorted(db.items(), key=lambda channel: channel[0][1]):
|
|
|
|
s = '\x02%s%s\x02 ' % k
|
|
|
|
if v['links']:
|
|
|
|
s += ' '.join([''.join(link) for link in v['links']])
|
|
|
|
else:
|
|
|
|
s += '(no relays yet)'
|
|
|
|
utils.msg(irc, source, s)
|