3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-30 23:09:23 +01:00

Merge branch 'master' into wip/relay-no-duplicate-modes

This commit is contained in:
James Lu 2015-07-25 10:18:08 -07:00
commit 25da086a6b
12 changed files with 274 additions and 110 deletions

View File

@ -18,7 +18,7 @@ Dependencies currently include:
#### Supported IRCds #### Supported IRCds
* InspIRCd 2.0.x - module: `inspircd` * InspIRCd 2.0.x - module: `inspircd`
* charybdis (3.5.x / git master) - module: `ts6` * charybdis (3.5.x / git master) - module: `ts6` (**experimental**)
### Installation ### Installation

View File

@ -1,4 +1,3 @@
from collections import defaultdict
import threading import threading
from random import randint from random import randint
@ -9,7 +8,7 @@ import time
class IrcUser(): class IrcUser():
def __init__(self, nick, ts, uid, ident='null', host='null', def __init__(self, nick, ts, uid, ident='null', host='null',
realname='PyLink dummy client', realhost='null', realname='PyLink dummy client', realhost='null',
ip='0.0.0.0', modes=set()): ip='0.0.0.0'):
self.nick = nick self.nick = nick
self.ts = ts self.ts = ts
self.uid = uid self.uid = uid
@ -18,7 +17,7 @@ class IrcUser():
self.realhost = realhost self.realhost = realhost
self.ip = ip self.ip = ip
self.realname = realname self.realname = realname
self.modes = modes self.modes = set()
self.identified = False self.identified = False
self.channels = set() self.channels = set()
@ -39,7 +38,6 @@ class IrcServer():
self.users = [] self.users = []
self.internal = internal self.internal = internal
self.name = name.lower() self.name = name.lower()
self.has_bursted = False
def __repr__(self): def __repr__(self):
return repr(self.__dict__) return repr(self.__dict__)

View File

@ -1,3 +1,8 @@
# This is a sample configuration file for PyLink. You'll likely want to rename it to config.yml
# and begin your configuration there.
# Note: lines starting with a "#" are comments and will be ignored.
bot: bot:
# Sets nick, user/ident, and real name. # Sets nick, user/ident, and real name.
nick: pylink nick: pylink

View File

@ -36,3 +36,61 @@ def handle_commands(irc, source, command, args):
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e))) utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
return return
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Return WHOIS replies to IRCds that use them.
def handle_whois(irc, source, command, args):
target = args['target']
user = irc.users.get(target)
if user is None:
log.warning('(%s) Got a WHOIS request for %r from %r, but the target doesn\'t exist in irc.users!', irc.name, target, source)
f = irc.proto.numericServer
server = utils.clientToServer(irc, target) or irc.sid
nick = user.nick
sourceisOper = ('o', None) in irc.users[source].modes
# https://www.alien.net.au/irc/irc2numerics.html
# 311: sends nick!user@host information
f(irc, server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname))
# 312: sends the server the target is on, and the name
f(irc, server, 312, source, "%s %s :PyLink Server" % (nick, irc.serverdata['hostname']))
# 313: sends a string denoting the target's operator privilege;
# we'll only send it if the user has umode +o.
if ('o', None) in user.modes:
f(irc, server, 313, source, "%s :is an IRC Operator" % nick)
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only shown to opers!
if sourceisOper:
f(irc, server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes)))
# 319: RPL_WHOISCHANNELS, shows channel list
public_chans = []
for chan in user.channels:
# Here, we'll want to hide secret/private channels from non-opers
# who are not in them.
c = irc.channels[chan]
if ((irc.cmodes.get('secret'), None) in c.modes or \
(irc.cmodes.get('private'), None) in c.modes) \
and not (sourceisOper or source in c.users):
continue
# TODO: show prefix modes like a regular IRCd does.
public_chans.append(chan)
if public_chans:
f(irc, server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
# 317: shows idle and signon time. Though we don't track the user's real
# idle time; we just return 0.
# 317 GL GL 15 1437632859 :seconds idle, signon time
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
try:
# Iterate over plugin-created WHOIS handlers. They return a tuple
# or list with two arguments: the numeric, and the text to send.
for func in utils.whois_handlers:
res = func(irc, target)
if res:
num, text = res
f(irc, server, num, source, text)
except Exception as e:
# Again, we wouldn't want this to crash our service, in case
# something goes wrong!
log.exception('Error caught in WHOIS handler: %s', e)
finally:
# 318: End of WHOIS.
f(irc, server, 318, source, "%s :End of /WHOIS list" % nick)
utils.add_hook(handle_whois, 'WHOIS')

View File

@ -61,7 +61,7 @@ class Irc():
self.sid = self.serverdata["sid"] self.sid = self.serverdata["sid"]
self.botdata = conf['bot'] self.botdata = conf['bot']
self.proto = proto self.proto = proto
self.pingfreq = self.serverdata.get('pingfreq') or 10 self.pingfreq = self.serverdata.get('pingfreq') or 30
self.pingtimeout = self.pingfreq * 2 self.pingtimeout = self.pingfreq * 2
self.initVars() self.initVars()
@ -84,7 +84,9 @@ class Irc():
self.socket.settimeout(self.pingtimeout) self.socket.settimeout(self.pingtimeout)
self.proto.connect(self) self.proto.connect(self)
self.spawnMain() self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing() self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name)
self.run() self.run()
except (socket.error, classes.ProtocolError, ConnectionError) as e: except (socket.error, classes.ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s', log.warning('(%s) Disconnected from IRC: %s: %s',
@ -183,7 +185,8 @@ class Irc():
nick = self.botdata.get('nick') or 'PyLink' nick = self.botdata.get('nick') or 'PyLink'
ident = self.botdata.get('ident') or 'pylink' ident = self.botdata.get('ident') or 'pylink'
host = self.serverdata["hostname"] host = self.serverdata["hostname"]
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("o", None)}) log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
for chan in self.serverdata['channels']: for chan in self.serverdata['channels']:
self.proto.joinClient(self, self.pseudoclient.uid, chan) self.proto.joinClient(self, self.pseudoclient.uid, chan)

View File

@ -49,7 +49,7 @@ def spawnclient(irc, source, args):
def quit(irc, source, args): def quit(irc, source, args):
"""<target> [<reason>] """<target> [<reason>]
Admin-only. Quits the PyLink client <target>, if it exists.""" Admin-only. Quits the PyLink client with nick <target>, if one exists."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
nick = args[0] nick = args[0]
@ -66,7 +66,7 @@ def quit(irc, source, args):
def joinclient(irc, source, args): def joinclient(irc, source, args):
"""<target> <channel1>,[<channel2>], etc. """<target> <channel1>,[<channel2>], etc.
Admin-only. Joins <target>, a PyLink client, to a comma-separated list of channels.""" Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
nick = args[0] nick = args[0]
@ -108,7 +108,7 @@ def nick(irc, source, args):
def part(irc, source, args): def part(irc, source, args):
"""<target> <channel1>,[<channel2>],... [<reason>] """<target> <channel1>,[<channel2>],... [<reason>]
Admin-only. Parts <target>, a PyLink client, from a comma-separated list of channels.""" Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
nick = args[0] nick = args[0]
@ -128,7 +128,7 @@ def part(irc, source, args):
def kick(irc, source, args): def kick(irc, source, args):
"""<source> <channel> <user> [<reason>] """<source> <channel> <user> [<reason>]
Admin-only. Kicks <user> from <channel> via <source>, where <source> is a PyLink client.""" Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
nick = args[0] nick = args[0]
@ -186,7 +186,7 @@ def showchan(irc, source, args):
def mode(irc, source, args): def mode(irc, source, args):
"""<source> <target> <modes> """<source> <target> <modes>
Admin-only. Sets modes <modes> on <target>.""" Admin-only. Sets modes <modes> on <target> from <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source) checkauthenticated(irc, source)
try: try:
modesource, target, modes = args[0], args[1], args[2:] modesource, target, modes = args[0], args[1], args[2:]

View File

@ -1,7 +1,6 @@
# commands.py: base PyLink commands # commands.py: base PyLink commands
import sys import sys
import os import os
import logging
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils import utils

View File

@ -5,7 +5,6 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pickle import pickle
import sched import sched
import threading import threading
import time
import string import string
from collections import defaultdict from collections import defaultdict
@ -15,6 +14,17 @@ from log import log
dbname = "pylinkrelay.db" dbname = "pylinkrelay.db"
relayusers = defaultdict(dict) relayusers = defaultdict(dict)
def relayWhoisHandlers(irc, target):
user = irc.users[target]
orig = getLocalUser(irc, target)
if orig:
network, remoteuid = orig
remotenick = utils.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)]
utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None): def normalizeNick(irc, netname, nick, separator=None):
# Block until we know the IRC network's nick length (after capabilities # Block until we know the IRC network's nick length (after capabilities
# are sent) # are sent)
@ -97,8 +107,7 @@ def save(irc, source, args):
def getPrefixModes(irc, remoteirc, channel, user): def getPrefixModes(irc, remoteirc, channel, user):
modes = '' modes = ''
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'): for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
if pmode not in remoteirc.cmodes: # Mode not supported by IRCd if pmode in remoteirc.cmodes: # Mode supported by IRCd
continue
mlist = irc.channels[channel].prefixmodes[pmode+'s'] mlist = irc.channels[channel].prefixmodes[pmode+'s']
log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist) log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist)
if user in mlist: if user in mlist:
@ -133,7 +142,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
modes = getSupportedUmodes(irc, remoteirc, userobj.modes) modes = getSupportedUmodes(irc, remoteirc, userobj.modes)
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
host=host, realname=realname, host=host, realname=realname,
modes=modes).uid modes=modes, ts=userobj.ts).uid
remoteirc.users[u].remote = irc.name remoteirc.users[u].remote = irc.name
relayusers[(irc.name, user)][remoteirc.name] = u relayusers[(irc.name, user)][remoteirc.name] = u
remoteirc.users[u].remote = irc.name remoteirc.users[u].remote = irc.name
@ -218,6 +227,7 @@ def initializeChannel(irc, channel):
all_links = db[relay]['links'].copy() all_links = db[relay]['links'].copy()
all_links.update((relay,)) all_links.update((relay,))
log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links) log.debug('(%s) initializeChannel: all_links: %s', irc.name, all_links)
# Iterate over all the remote channels linked in this relay.
for link in all_links: for link in all_links:
modes = [] modes = []
remotenet, remotechan = link remotenet, remotechan = link
@ -229,20 +239,9 @@ def initializeChannel(irc, channel):
rc = remoteirc.channels[remotechan] rc = remoteirc.channels[remotechan]
if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)): if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)):
continue # They aren't connected, don't bother! continue # They aren't connected, don't bother!
for user in remoteirc.channels[remotechan].users: # Join their (remote) users and set their modes.
# Don't spawn our pseudoclients again. relayJoins(remoteirc, remotechan, rc.users,
if not utils.isInternalClient(remoteirc, user): rc.ts, rc.modes)
log.debug('(%s) initializeChannel: should be joining %s/%s to %s', irc.name, user, remotenet, channel)
localuser = getRemoteUser(remoteirc, irc, user)
if localuser is None:
log.warning('(%s) got None for local user for %s/%s', irc.name, user, remotenet)
continue
userpair = (getPrefixModes(remoteirc, irc, remotechan, user), localuser)
log.debug('(%s) initializeChannel: adding %s to queued_users for %s', irc.name, userpair, channel)
queued_users.append(userpair)
if queued_users:
irc.proto.sjoinServer(irc, irc.sid, channel, queued_users, ts=rc.ts)
relayModes(remoteirc, irc, remoteirc.sid, remotechan)
relayModes(irc, remoteirc, irc.sid, channel) relayModes(irc, remoteirc, irc.sid, channel)
topic = remoteirc.channels[relay[1]].topic topic = remoteirc.channels[relay[1]].topic
# Only update the topic if it's different from what we already have, # Only update the topic if it's different from what we already have,
@ -251,6 +250,7 @@ def initializeChannel(irc, channel):
irc.proto.topicServer(irc, irc.sid, channel, topic) irc.proto.topicServer(irc, irc.sid, channel, topic)
log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users) log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users)
# After that's done, we'll send our users to them.
relayJoins(irc, channel, c.users, c.ts, c.modes) relayJoins(irc, channel, c.users, c.ts, c.modes)
irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) irc.proto.joinClient(irc, irc.pseudoclient.uid, channel)
@ -262,7 +262,6 @@ def handle_join(irc, numeric, command, args):
modes = args['modes'] modes = args['modes']
ts = args['ts'] ts = args['ts']
users = set(args['users']) users = set(args['users'])
# users.update(irc.channels[channel].users)
relayJoins(irc, channel, users, ts, modes) relayJoins(irc, channel, users, ts, modes)
utils.add_hook(handle_join, 'JOIN') utils.add_hook(handle_join, 'JOIN')
@ -296,6 +295,8 @@ def handle_part(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].copy().items(): for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
remotechan = findRemoteChan(irc, remoteirc, channel) remotechan = findRemoteChan(irc, remoteirc, channel)
if remotechan is None:
continue
remoteirc.proto.partClient(remoteirc, user, remotechan, text) remoteirc.proto.partClient(remoteirc, user, remotechan, text)
if not remoteirc.users[user].channels: if not remoteirc.users[user].channels:
remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.') remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.')
@ -310,7 +311,10 @@ def handle_privmsg(irc, numeric, command, args):
return return
sent = 0 sent = 0
relay = findRelay((irc.name, target)) relay = findRelay((irc.name, target))
if utils.isChannel(target) and relay and not db[relay]['links']: # Don't send any "you must be in common channels" if we're not part
# of a relay, or we are but there are no links!
if utils.isChannel(target) and ((relay and not db[relay]['links']) or \
relay is None):
return return
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
@ -416,6 +420,12 @@ def handle_kick(irc, source, command, args):
text = "(<unknown kicker>@%s) %s" % (irc.name, text) text = "(<unknown kicker>@%s) %s" % (irc.name, text)
remoteirc.proto.kickServer(remoteirc, remoteirc.sid, remoteirc.proto.kickServer(remoteirc, remoteirc.sid,
remotechan, real_target, text) remotechan, real_target, text)
if target != irc.pseudoclient.uid and not irc.users[target].channels:
irc.proto.quitClient(irc, target, 'Left all shared channels.')
remoteuser = getLocalUser(irc, target)
del relayusers[remoteuser][irc.name]
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
def handle_chgclient(irc, source, command, args): def handle_chgclient(irc, source, command, args):
@ -434,8 +444,10 @@ def handle_chgclient(irc, source, command, args):
remoteirc = utils.networkobjects[netname] remoteirc = utils.networkobjects[netname]
try: try:
remoteirc.proto.updateClient(remoteirc, user, field, text) remoteirc.proto.updateClient(remoteirc, user, field, text)
except ValueError: # IRCd doesn't support changing the field we want except NotImplementedError: # 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) log.debug('(%s) Ignoring changing field %r of %s on %s (for %s/%s);'
' remote IRCd doesn\'t support it', irc.name, field,
user, target, netname, irc.name)
continue continue
for c in ('CHGHOST', 'CHGNAME', 'CHGIDENT'): for c in ('CHGHOST', 'CHGNAME', 'CHGIDENT'):
@ -446,10 +458,10 @@ whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception',
'limit', 'moderated', 'noctcp', 'noextmsg', 'nokick', 'limit', 'moderated', 'noctcp', 'noextmsg', 'nokick',
'noknock', 'nonick', 'nonotice', 'op', 'operonly', 'noknock', 'nonick', 'nonotice', 'op', 'operonly',
'opmoderated', 'owner', 'private', 'regonly', 'opmoderated', 'owner', 'private', 'regonly',
'regmoderated', 'secret', 'sslonly', 'regmoderated', 'secret', 'sslonly', 'adminonly',
'stripcolor', 'topiclock', 'voice'} 'stripcolor', 'topiclock', 'voice'}
whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper', whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper',
'regdeaf', 'u_stripcolor', 'servprotect', 'u_noctcp'} 'regdeaf', 'u_stripcolor', 'u_noctcp', 'wallops'}
def relayModes(irc, remoteirc, sender, channel, modes=None): def relayModes(irc, remoteirc, sender, channel, modes=None):
remotechan = findRemoteChan(irc, remoteirc, channel) remotechan = findRemoteChan(irc, remoteirc, channel)
log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan) log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan)
@ -590,30 +602,40 @@ utils.add_hook(handle_topic, 'TOPIC')
def handle_kill(irc, numeric, command, args): def handle_kill(irc, numeric, command, args):
target = args['target'] target = args['target']
userdata = args['userdata'] userdata = args['userdata']
if numeric not in irc.users:
# A server's sending kill? Uh oh, this can't be good.
return
# 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) realuser = getLocalUser(irc, target)
log.debug('(%s) relay handle_kill: realuser is %r', irc.name, realuser)
# 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] del relayusers[realuser][irc.name]
remoteirc = utils.networkobjects[realuser[0]] remoteirc = utils.networkobjects[realuser[0]]
for channel in remoteirc.channels: for channel in remoteirc.channels:
remotechan = findRemoteChan(remoteirc, irc, channel) remotechan = findRemoteChan(remoteirc, irc, channel)
if remotechan: if remotechan:
modes = getPrefixModes(remoteirc, irc, remotechan, realuser[1]) modes = getPrefixModes(remoteirc, irc, remotechan, realuser[1])
log.debug('(%s) handle_kill: userpair: %s, %s', irc.name, modes, realuser) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser)
client = getRemoteUser(remoteirc, irc, realuser[1]) client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)]) irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)])
if userdata and numeric in irc.users:
utils.msg(irc, numeric, "Your kill has to %s been blocked " utils.msg(irc, numeric, "Your kill has to %s been blocked "
"because PyLink does not allow killing" "because PyLink does not allow killing"
" users over the relay at this time." % \ " users over the relay at this time." % \
userdata.nick, notice=True) userdata.nick, notice=True)
# 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']})
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
def relayJoins(irc, channel, users, ts, modes): def relayJoins(irc, channel, users, ts, modes):
queued_users = []
for name, remoteirc in utils.networkobjects.items(): for name, remoteirc in utils.networkobjects.items():
queued_users = []
if name == irc.name: if name == irc.name:
# Don't relay things to their source network... # Don't relay things to their source network...
continue continue
@ -622,18 +644,19 @@ def relayJoins(irc, channel, users, ts, modes):
# If there is no link on our network for the user, don't # If there is no link on our network for the user, don't
# bother spawning it. # bother spawning it.
continue continue
log.debug('(%s) relayJoins: got %r for users', irc.name, users)
for user in users.copy(): for user in users.copy():
if utils.isInternalClient(irc, user) or user not in irc.users:
# We don't need to clone PyLink pseudoclients... That's
# meaningless.
continue
try: try:
if irc.users[user].remote: if irc.users[user].remote:
# Is the .remote attribute set? If so, don't relay already # Is the .remote attribute set? If so, don't relay already
# relayed clients; that'll trigger an endless loop! # relayed clients; that'll trigger an endless loop!
continue continue
except (AttributeError, KeyError): # Nope, it isn't. except AttributeError: # Nope, it isn't.
pass pass
if utils.isInternalClient(irc, user) or user not in irc.users:
# We don't need to clone PyLink pseudoclients... That's
# meaningless.
continue
log.debug('Okay, spawning %s/%s everywhere', user, irc.name) log.debug('Okay, spawning %s/%s everywhere', user, irc.name)
assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user) assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user)
u = getRemoteUser(irc, remoteirc, user) u = getRemoteUser(irc, remoteirc, user)
@ -673,9 +696,14 @@ def removeChannel(irc, channel):
for user in irc.channels[channel].users.copy(): for user in irc.channels[channel].users.copy():
if not utils.isInternalClient(irc, user): if not utils.isInternalClient(irc, user):
relayPart(irc, channel, user) relayPart(irc, channel, user)
# Don't ever part the main client from any of its autojoin channels.
else: else:
if user == irc.pseudoclient.uid and channel in \
irc.serverdata['channels']:
continue
irc.proto.partClient(irc, user, channel, 'Channel delinked.') irc.proto.partClient(irc, user, channel, 'Channel delinked.')
if not irc.users[user].channels: # Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels:
irc.proto.quitClient(irc, user, 'Left all shared channels.') irc.proto.quitClient(irc, user, 'Left all shared channels.')
remoteuser = getLocalUser(irc, user) remoteuser = getLocalUser(irc, user)
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]
@ -853,18 +881,24 @@ def handle_disconnect(irc, numeric, command, args):
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
def handle_save(irc, numeric, command, args): def handle_save(irc, numeric, command, args):
# Nick collision! Try to change our nick to the next available normalized
# nick.
target = args['target'] target = args['target']
if utils.isInternalClient(irc, target):
realuser = getLocalUser(irc, target) realuser = getLocalUser(irc, target)
if realuser is None: log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r',
return irc.name, target, realuser)
if utils.isInternalClient(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 remotenet, remoteuser = realuser
remoteirc = utils.networkobjects[remotenet] remoteirc = utils.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick nick = remoteirc.users[remoteuser].nick
newnick = normalizeNick(irc, remotenet, nick) newnick = normalizeNick(irc, remotenet, nick)
irc.proto.nickClient(irc, target, newnick) irc.proto.nickClient(irc, target, newnick)
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})
utils.add_hook(handle_save, "SAVE") utils.add_hook(handle_save, "SAVE")
@utils.add_cmd @utils.add_cmd

View File

@ -10,6 +10,8 @@ from log import log
from classes import * from classes import *
casemapping = 'rfc1459'
# Raw commands sent from servers vary from protocol to protocol. Here, we map # Raw commands sent from servers vary from protocol to protocol. Here, we map
# non-standard names to our hook handlers, so plugins get the information they need. # non-standard names to our hook handlers, so plugins get the information they need.
@ -23,7 +25,7 @@ def _send(irc, sid, msg):
irc.send(':%s %s' % (sid, msg)) irc.send(':%s %s' % (sid, msg))
def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None): server=None, ip='0.0.0.0', realname=None, ts=None):
server = server or irc.sid server = server or irc.sid
if not utils.isInternalServer(irc, server): if not utils.isInternalServer(irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
@ -32,12 +34,13 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
if server not in irc.uidgen: if server not in irc.uidgen:
irc.uidgen[server] = utils.TS6UIDGenerator(server) irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = irc.uidgen[server].next_uid() uid = irc.uidgen[server].next_uid()
ts = int(time.time()) ts = ts or int(time.time())
realname = realname or irc.botdata['realname'] realname = realname or irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, modes=modes) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes)
irc.servers[server].users.append(uid) irc.servers[server].users.append(uid)
_send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" _send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
" {ts} {modes} + :{realname}".format(ts=ts, host=host, " {ts} {modes} + :{realname}".format(ts=ts, host=host,
@ -273,16 +276,16 @@ def updateClient(irc, numeric, field, text):
Changes the <field> field of <target> PyLink PseudoClient <client numeric>.""" Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
field = field.upper() field = field.upper()
if field == 'IDENT': if field == 'IDENT':
handle_fident(irc, numeric, 'PYLINK_UPDATECLIENT_IDENT', [text]) irc.users[numeric].ident = text
_send(irc, numeric, 'FIDENT %s' % text) _send(irc, numeric, 'FIDENT %s' % text)
elif field == 'HOST': elif field == 'HOST':
handle_fhost(irc, numeric, 'PYLINK_UPDATECLIENT_HOST', [text]) irc.users[numeric].host = text
_send(irc, numeric, 'FHOST %s' % text) _send(irc, numeric, 'FHOST %s' % text)
elif field in ('REALNAME', 'GECOS'): elif field in ('REALNAME', 'GECOS'):
handle_fname(irc, numeric, 'PYLINK_UPDATECLIENT_GECOS', [text]) irc.users[numeric].realname = text
_send(irc, numeric, 'FNAME :%s' % text) _send(irc, numeric, 'FNAME :%s' % text)
else: else:
raise ValueError("Changing field %r of a client is unsupported by this protocol." % field) raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
def pingServer(irc, source=None, target=None): def pingServer(irc, source=None, target=None):
source = source or irc.sid source = source or irc.sid
@ -290,6 +293,12 @@ def pingServer(irc, source=None, target=None):
if not (target is None or source is None): if not (target is None or source is None):
_send(irc, source, 'PING %s %s' % (source, target)) _send(irc, source, 'PING %s %s' % (source, target))
def numericServer(irc, source, numeric, text):
raise NotImplementedError("Numeric sending is not yet implemented by this "
"protocol module. WHOIS requests are handled "
"locally by InspIRCd servers, so there is no "
"need for PyLink to send numerics directly yet.")
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
irc.uidgen = {} irc.uidgen = {}
@ -318,7 +327,8 @@ def handle_privmsg(irc, source, command, args):
def handle_kill(irc, source, command, args): def handle_kill(irc, source, command, args):
killed = args[0] killed = args[0]
data = irc.users[killed] data = irc.users.get(killed)
if data:
removeClient(irc, killed) removeClient(irc, killed)
return {'target': killed, 'text': args[1], 'userdata': data} return {'target': killed, 'text': args[1], 'userdata': data}
@ -467,7 +477,7 @@ def handle_events(irc, data):
# Each server message looks something like this: # Each server message looks something like this:
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE
# :<sid> <command> <argument1> <argument2> ... :final multi word argument # :<sid> <command> <argument1> <argument2> ... :final multi word argument
args = data.split() args = data.split(" ")
if not args: if not args:
# No data?? # No data??
return return
@ -559,7 +569,7 @@ def handle_events(irc, data):
if parsed_args is not None: if parsed_args is not None:
return [numeric, command, parsed_args] return [numeric, command, parsed_args]
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst=True): def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server'):
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver # -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
uplink = uplink or irc.sid uplink = uplink or irc.sid
name = name.lower() name = name.lower()
@ -578,13 +588,8 @@ def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst
raise ValueError('Invalid server name %r' % name) raise ValueError('Invalid server name %r' % name)
_send(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) _send(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
irc.servers[sid] = IrcServer(uplink, name, internal=True) irc.servers[sid] = IrcServer(uplink, name, internal=True)
if endburst:
endburstServer(irc, sid)
return sid
def endburstServer(irc, sid):
_send(irc, sid, 'ENDBURST') _send(irc, sid, 'ENDBURST')
irc.servers[sid].has_bursted = True return sid
def handle_ftopic(irc, numeric, command, args): def handle_ftopic(irc, numeric, command, args):
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic # <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic

View File

@ -2,7 +2,6 @@ import time
import sys import sys
import os import os
import re import re
from copy import copy
curdir = os.path.dirname(__file__) curdir = os.path.dirname(__file__)
sys.path += [curdir, os.path.dirname(curdir)] sys.path += [curdir, os.path.dirname(curdir)]
@ -17,13 +16,14 @@ from inspircd import handle_privmsg, handle_kill, handle_kick, handle_error, \
handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \ handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \
handle_notice handle_notice
casemapping = 'rfc1459'
hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'}
def _send(irc, sid, msg): def _send(irc, sid, msg):
irc.send(':%s %s' % (sid, msg)) irc.send(':%s %s' % (sid, msg))
def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None): server=None, ip='0.0.0.0', realname=None, ts=None):
server = server or irc.sid server = server or irc.sid
if not utils.isInternalServer(irc, server): if not utils.isInternalServer(irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
@ -32,20 +32,22 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
if server not in irc.uidgen: if server not in irc.uidgen:
irc.uidgen[server] = utils.TS6UIDGenerator(server) irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = irc.uidgen[server].next_uid() uid = irc.uidgen[server].next_uid()
# UID: # EUID:
# parameters: nickname, hopcount, nickTS, umodes, username, # parameters: nickname, hopcount, nickTS, umodes, username,
# visible hostname, IP address, UID, gecos # visible hostname, IP address, UID, real hostname, account name, gecos
ts = int(time.time()) ts = ts or int(time.time())
realname = realname or irc.botdata['realname'] realname = realname or irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, modes=modes) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes)
irc.servers[server].users.append(uid) irc.servers[server].users.append(uid)
_send(irc, server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " _send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
":{realname}".format(ts=ts, host=host, "{realhost} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname)) modes=raw_modes, ip=ip, realname=realname,
realhost=realhost))
return u return u
def joinClient(irc, client, channel): def joinClient(irc, client, channel):
@ -214,7 +216,7 @@ def updateClient(irc, numeric, field, text):
Changes the <field> field of <target> PyLink PseudoClient <client numeric>.""" Changes the <field> field of <target> PyLink PseudoClient <client numeric>."""
field = field.upper() field = field.upper()
if field == 'HOST': if field == 'HOST':
handle_chghost(irc, numeric, 'PYLINK_UPDATECLIENT_HOST', [text]) irc.users[numeric].host = text
_send(irc, irc.sid, 'CHGHOST %s :%s' % (numeric, text)) _send(irc, irc.sid, 'CHGHOST %s :%s' % (numeric, text))
else: else:
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
@ -228,6 +230,9 @@ def pingServer(irc, source=None, target=None):
else: else:
_send(irc, source, 'PING %s' % source) _send(irc, source, 'PING %s' % source)
def numericServer(irc, source, numeric, target, text):
_send(irc, source, '%s %s %s' % (numeric, target, text))
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
irc.uidgen = {} irc.uidgen = {}
@ -375,8 +380,12 @@ def handle_euid(irc, numeric, command, args):
irc.servers[numeric].users.append(uid) irc.servers[numeric].users.append(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_uid(irc, numeric, command, args):
raise ProtocolError("Servers must use EUID to send users! This is a "
"requested capability; plain UID (received) is not "
"handled by us at all!")
def handle_server(irc, numeric, command, args): def handle_server(irc, numeric, command, args):
# SERVER is sent by our uplink or any other server to introduce others.
# parameters: server name, hopcount, sid, server description # parameters: server name, hopcount, sid, server description
servername = args[0].lower() servername = args[0].lower()
try: try:
@ -390,6 +399,8 @@ def handle_server(irc, numeric, command, args):
irc.servers[sid] = IrcServer(numeric, servername) irc.servers[sid] = IrcServer(numeric, servername)
return {'name': servername, 'sid': sid, 'text': sdesc} return {'name': servername, 'sid': sid, 'text': sdesc}
handle_sid = handle_server
def handle_tmode(irc, numeric, command, args): def handle_tmode(irc, numeric, command, args):
# <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4
channel = args[1].lower() channel = args[1].lower()
@ -403,7 +414,7 @@ def handle_events(irc, data):
# TS6 messages: # TS6 messages:
# :42X COMMAND arg1 arg2 :final long arg # :42X COMMAND arg1 arg2 :final long arg
# :42XAAAAAA PRIVMSG #somewhere :hello! # :42XAAAAAA PRIVMSG #somewhere :hello!
args = data.split() args = data.split(" ")
if not args: if not args:
# No data?? # No data??
return return
@ -428,6 +439,12 @@ def handle_events(irc, data):
# According to the TS6 protocol documentation, we should send SVINFO # According to the TS6 protocol documentation, we should send SVINFO
# when we get our uplink's SERVER command. # when we get our uplink's SERVER command.
irc.send('SVINFO 6 6 0 :%s' % int(time.time())) irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
elif args[0] == 'SQUIT':
# What? Charybdis send this in a different format!
# <- SQUIT 00A :Remote host closed the connection
split_server = args[1]
res = handle_squit(irc, split_server, 'SQUIT', [split_server])
irc.callHooks([split_server, 'SQUIT', res])
elif args[0] == 'CAPAB': elif args[0] == 'CAPAB':
# We only get a list of keywords here. Charybdis obviously assumes that # We only get a list of keywords here. Charybdis obviously assumes that
# we know what modes it supports (indeed, this is a standard list). # we know what modes it supports (indeed, this is a standard list).
@ -447,6 +464,10 @@ def handle_events(irc, data):
# https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80
chary_cmodes = { # TS6 generic modes: chary_cmodes = { # TS6 generic modes:
# Note: charybdis +p has the effect of being both
# noknock AND private. Surprisingly, mapping it twice
# works pretty well: setting +p on a charybdis relay
# server sets +pK on an InspIRCd network.
'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit': 'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit':
'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p', 'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p',
'secret': 's', 'topiclock': 't', 'secret': 's', 'topiclock': 't',
@ -454,7 +475,9 @@ def handle_events(irc, data):
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F', 'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P', 'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
'c_noforwards': 'Q', 'stripcolor': 'c', 'allowinvite': 'c_noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
'g', 'opmoderated': 'z', 'g', 'opmoderated': 'z', 'noctcp': 'C',
# charybdis-specific modes provided by EXTENSIONS
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
# Now, map all the ABCD type modes: # Now, map all the ABCD type modes:
'*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'mnprst'} '*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'mnprst'}
if 'EX' in caps: if 'EX' in caps:
@ -526,7 +549,7 @@ def handle_events(irc, data):
if parsed_args is not None: if parsed_args is not None:
return [numeric, command, parsed_args] return [numeric, command, parsed_args]
def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server', endburst=True): def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server'):
# -> :0AL SERVER test.server 1 0XY :some silly pseudoserver # -> :0AL SERVER test.server 1 0XY :some silly pseudoserver
uplink = uplink or irc.sid uplink = uplink or irc.sid
name = name.lower() name = name.lower()
@ -584,3 +607,23 @@ def handle_bmask(irc, numeric, command, args):
modes.append(('+%s' % mode, ban)) modes.append(('+%s' % mode, ban))
utils.applyModes(irc, channel, modes) utils.applyModes(irc, channel, modes)
return {'target': channel, 'modes': modes, 'ts': ts} return {'target': channel, 'modes': modes, 'ts': ts}
def handle_whois(irc, numeric, command, args):
# <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel
return {'target': args[0]}
def handle_472(irc, numeric, command, args):
# <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me
# 472 is sent to us when one of our clients tries to set a mode the server
# doesn't support. In this case, we'll raise a warning to alert the user
# about it.
badmode = args[1]
reason = args[-1]
setter = args[0]
charlist = {'A': 'chm_adminonly', 'O': 'chm_operonly', 'S': 'chm_sslonly'}
if badmode in charlist:
log.warning('(%s) User %r attempted to set channel mode %r, but the '
'extension providing it isn\'t loaded! To prevent possible'
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
'your IRCd configuration.', irc.name, setter, badmode,
charlist[badmode])

View File

@ -2,7 +2,6 @@ import sys
import os import os
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')] sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
import unittest import unittest
import time
from collections import defaultdict from collections import defaultdict
import inspircd import inspircd
@ -72,7 +71,6 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertNotIn(u, self.irc.channels['#channel'].users) self.assertNotIn(u, self.irc.channels['#channel'].users)
self.assertNotIn(u, self.irc.users) self.assertNotIn(u, self.irc.users)
self.assertNotIn(u, self.irc.servers[self.irc.sid].users) self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
pass
def testKickClient(self): def testKickClient(self):
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid

View File

@ -12,6 +12,7 @@ command_hooks = defaultdict(list)
networkobjects = {} networkobjects = {}
schedulers = {} schedulers = {}
plugins = [] plugins = []
whois_handlers = []
started = threading.Event() started = threading.Event()
class TS6UIDGenerator(): class TS6UIDGenerator():
@ -97,8 +98,6 @@ class TS6SIDGenerator():
self.iters[pos] = iter(self.allowedchars[pos]) self.iters[pos] = iter(self.allowedchars[pos])
next(self.iters[pos]) next(self.iters[pos])
self.increment(pos-1) self.increment(pos-1)
else:
print('NEXT')
def next_sid(self): def next_sid(self):
sid = ''.join(self.output) sid = ''.join(self.output)
@ -122,9 +121,18 @@ def add_hook(func, command):
command = command.upper() command = command.upper()
command_hooks[command].append(func) command_hooks[command].append(func)
def toLower(irc, text):
if irc.proto.casemapping == 'rfc1459':
text = text.replace('{', '[')
text = text.replace('}', ']')
text = text.replace('|', '\\')
text = text.replace('~', '^')
return text.lower()
def nickToUid(irc, nick): def nickToUid(irc, nick):
nick = toLower(irc, nick)
for k, v in irc.users.items(): for k, v in irc.users.items():
if v.nick == nick: if toLower(irc, v.nick) == nick:
return k return k
def clientToServer(irc, numeric): def clientToServer(irc, numeric):
@ -170,16 +178,18 @@ def parseModes(irc, target, args):
if usermodes: if usermodes:
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes) log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes)
supported_modes = irc.umodes supported_modes = irc.umodes
oldmodes = irc.users[target].modes
else: else:
log.debug('(%s) Using irc.cmodes for this query: %s', irc.name, irc.cmodes) log.debug('(%s) Using irc.cmodes for this query: %s', irc.name, irc.cmodes)
supported_modes = irc.cmodes supported_modes = irc.cmodes
oldmodes = irc.channels[target].modes
res = [] res = []
for mode in modestring: for mode in modestring:
if mode in '+-': if mode in '+-':
prefix = mode prefix = mode
else: else:
if not prefix: if not prefix:
raise ValueError('Invalid query %r: mode char given without preceding prefix.' % modestring) prefix = '+'
arg = None arg = None
log.debug('Current mode: %s%s; args left: %s', prefix, mode, args) log.debug('Current mode: %s%s; args left: %s', prefix, mode, args)
try: try:
@ -187,6 +197,17 @@ def parseModes(irc, target, args):
# Must have parameter. # Must have parameter.
log.debug('Mode %s: This mode must have parameter.', mode) log.debug('Mode %s: This mode must have parameter.', mode)
arg = args.pop(0) arg = args.pop(0)
if prefix == '-' and mode in supported_modes['*B'] and arg == '*':
# Charybdis allows unsetting +k without actually
# knowing the key by faking the argument when unsetting
# as a single "*".
# We'd need to know the real argument of +k for us to
# be able to unset the mode.
oldargs = [m[1] for m in oldmodes if m[0] == mode]
if oldargs:
# Set the arg to the old one on the channel.
arg = oldargs[0]
log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg)
elif mode in irc.prefixmodes and not usermodes: elif mode in irc.prefixmodes and not usermodes:
# We're setting a prefix mode on someone (e.g. +o user1) # We're setting a prefix mode on someone (e.g. +o user1)
log.debug('Mode %s: This mode is a prefix mode.', mode) log.debug('Mode %s: This mode is a prefix mode.', mode)