ircutils: Add formatWhois function

Parsing through the various WHOIS replies to build a formatted string
isn't a trivial task, especially since there is some privacy related
information.  Consolidate this handling into a single function so
there's one place to fix bugs.

Also fix an issue with people putting (unterminated) formatted text into
the "realname" field of their IRC client (c.f., ProgVal/Limnoria#1083).

Signed-off-by: James McCoy <vega.james@gmail.com>
This commit is contained in:
James McCoy 2015-03-26 00:11:36 -04:00
parent a8cd99f121
commit c3695c9419
No known key found for this signature in database
GPG Key ID: DFE691AE331BA3DB
3 changed files with 113 additions and 142 deletions

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2002-2004, Jeremiah Fincher
# Copyright (c) 2010, James McCoy # Copyright (c) 2010,2015 James McCoy
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -158,87 +158,9 @@ class Network(callbacks.Plugin):
if (irc, loweredNick) not in self._whois: if (irc, loweredNick) not in self._whois:
return return
(replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)]
hostmask = '@'.join(d['311'].args[2:4]) d['318'] = msg
user = d['311'].args[-1] s = ircutils.formatWhois(irc, d, caller=replyMsg.nick,
if '319' in d: channel=replyMsg.args[0])
channels = d['319'].args[-1].split()
ops = []
voices = []
normal = []
halfops = []
for channel in channels:
origchan = channel
channel = channel.lstrip('@%+~!')
# UnrealIRCd uses & for user modes and disallows it as a
# channel-prefix, flying in the face of the RFC. Have to
# handle this specially when processing WHOIS response.
testchan = channel.lstrip('&')
if testchan != channel and irc.isChannel(testchan):
channel = testchan
diff = len(channel) - len(origchan)
modes = origchan[:diff]
chan = irc.state.channels.get(channel)
# The user is in a channel the bot is in, so the ircd may have
# responded with otherwise private data.
if chan:
# Skip channels the callee isn't in. This helps prevents
# us leaking information when the channel is +s or the
# target is +i
if replyMsg.nick not in chan.users:
continue
# Skip +s channels the target is in only if the reply isn't
# being sent to that channel
if 's' in chan.modes and \
not ircutils.strEqual(replyMsg.args[0], channel):
continue
if not modes:
normal.append(channel)
elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')):
ops.append(channel[1:])
elif utils.iter.any(lambda c: c in modes, ('%',)):
halfops.append(channel[1:])
elif utils.iter.any(lambda c: c in modes, ('+',)):
voices.append(channel[1:])
L = []
if ops:
L.append(format('is an op on %L', ops))
if halfops:
L.append(format('is a halfop on %L', halfops))
if voices:
L.append(format('is voiced on %L', voices))
if normal:
if L:
L.append(format('is also on %L', normal))
else:
L.append(format('is on %L', normal))
else:
L = ['isn\'t on any non-secret channels']
channels = format('%L', L)
if '317' in d:
idle = utils.timeElapsed(d['317'].args[2])
signon = time.strftime(conf.supybot.reply.format.time(),
time.localtime(float(d['317'].args[3])))
else:
idle = '<unknown>'
signon = '<unknown>'
if '312' in d:
server = d['312'].args[2]
else:
server = '<unknown>'
if '301' in d:
away = ' %s is away: %s.' % (nick, d['301'].args[2])
else:
away = ''
if '320' in d:
if d['320'].args[2]:
identify = ' identified'
else:
identify = ''
else:
identify = ''
s = '%s (%s) has been%s on server %s since %s (idle for %s) and ' \
'%s.%s' % (user, hostmask, identify, server, signon, idle,
channels, away)
replyIrc.reply(s) replyIrc.reply(s)
del self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)]

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2002-2004, Jeremiah Fincher
# Copyright (c) 2010, James McCoy # Copyright (c) 2010,2015 James McCoy
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -196,64 +196,9 @@ class Relay(callbacks.Plugin):
if (irc, loweredNick) not in self._whois: if (irc, loweredNick) not in self._whois:
return return
(replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)]
hostmask = '@'.join(d['311'].args[2:4]) d['318'] = msg
user = d['311'].args[-1] s = ircutils.formatWhois(irc, d, caller=replyMsg.nick,
if '319' in d: channel=replyMsg.args[0])
channels = d['319'].args[-1].split()
ops = []
voices = []
normal = []
halfops = []
for channel in channels:
if channel.startswith('@'):
ops.append(channel[1:])
elif channel.startswith('%'):
halfops.append(channel[1:])
elif channel.startswith('+'):
voices.append(channel[1:])
else:
normal.append(channel)
L = []
if ops:
L.append(format('is an op on %L', ops))
if halfops:
L.append(format('is a halfop on %L', halfups))
if voices:
L.append(format('is voiced on %L', voices))
if normal:
if L:
L.append(format('is also on %L', normal))
else:
L.append(format('is on %L', normal))
else:
L = ['isn\'t on any non-secret channels']
channels = format('%L', L)
if '317' in d:
idle = utils.timeElapsed(d['317'].args[2])
signon = time.strftime(conf.supybot.reply.format.time(),
time.localtime(float(d['317'].args[3])))
else:
idle = '<unknown>'
signon = '<unknown>'
if '312' in d:
server = d['312'].args[2]
else:
server = '<unknown>'
if '301' in d:
away = format(' %s is away: %s.', nick, d['301'].args[2])
else:
away = ''
if '320' in d:
if d['320'].args[2]:
identify = ' identified'
else:
identify = ''
else:
identify = ''
s = format('%s (%s) has been%s on server %s since %s (idle for %s) '
'and %s.%s',
user, hostmask, identify, server, signon, idle,
channels, away)
replyIrc.reply(s) replyIrc.reply(s)
del self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)]

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2002-2005, Jeremiah Fincher
# Copyright (c) 2009,2011, James McCoy # Copyright (c) 2009,2011,2015 James McCoy
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -338,6 +338,110 @@ def stripFormatting(s):
s = stripUnderline(s) s = stripUnderline(s)
return s.replace('\x0f', '').replace('\x0F', '') return s.replace('\x0f', '').replace('\x0F', '')
_containsFormattingRe = re.compile(r'[\x02\x03\x16\x1f]')
def formatWhois(irc, replies, caller='', channel=''):
"""Returns a string describing the target of a WHOIS command.
Arguments are:
* irc: the irclib.Irc object on which the replies was received
* replies: a dict mapping the reply codes ('311', '312', etc.) to their
corresponding ircmsg.IrcMsg
* caller: an optional nick specifying who requested the whois information
* channel: an optional channel specifying where the reply will be sent
If provided, caller and channel will be used to avoid leaking information
that the caller/channel shouldn't be privy to.
"""
hostmask = '@'.join(replies['311'].args[2:4])
nick = replies['318'].args[1]
user = replies['311'].args[-1]
if _containsFormattingRe.search(user) and user[-1] != '\x0f':
# For good measure, disable any formatting
user = '%s\x0f' % user
if '319' in replies:
channels = replies['319'].args[-1].split()
ops = []
voices = []
normal = []
halfops = []
for chan in channels:
origchan = chan
chan = chan.lstrip('@%+~!')
# UnrealIRCd uses & for user modes and disallows it as a
# channel-prefix, flying in the face of the RFC. Have to
# handle this specially when processing WHOIS response.
testchan = chan.lstrip('&')
if testchan != chan and irc.isChannel(testchan):
chan = testchan
diff = len(chan) - len(origchan)
modes = origchan[:diff]
chanState = irc.state.channels.get(chan)
# The user is in a channel the bot is in, so the ircd may have
# responded with otherwise private data.
if chanState:
# Skip channels the callee isn't in. This helps prevents
# us leaking information when the channel is +s or the
# target is +i
if caller not in chanState.users:
continue
# Skip +s channels the target is in only if the reply isn't
# being sent to that channel
if 's' in chanState.modes and \
not ircutils.strEqual(channel or '', chan):
continue
if not modes:
normal.append(chan)
elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')):
ops.append(chan[1:])
elif utils.iter.any(lambda c: c in modes, ('%',)):
halfops.append(chan[1:])
elif utils.iter.any(lambda c: c in modes, ('+',)):
voices.append(chan[1:])
L = []
if ops:
L.append(format('is an op on %L', ops))
if halfops:
L.append(format('is a halfop on %L', halfops))
if voices:
L.append(format('is voiced on %L', voices))
if normal:
if L:
L.append(format('is also on %L', normal))
else:
L.append(format('is on %L', normal))
else:
L = ['isn\'t on any non-secret channels']
channels = format('%L', L)
if '317' in replies:
idle = utils.timeElapsed(replies['317'].args[2])
signon = utils.str.timestamp(float(replies['317'].args[3]))
else:
idle = '<unknown>'
signon = '<unknown>'
if '312' in replies:
server = replies['312'].args[2]
else:
server = '<unknown>'
if '301' in replies:
away = ' %s is away: %s.' % (nick, replies['301'].args[2])
else:
away = ''
if '320' in replies:
if replies['320'].args[2]:
identify = ' identified'
else:
identify = ''
else:
identify = ''
s = utils.str.format('%s (%s) has been%s on server %s since %s '
'(idle for %s) and %s.%s',
user, hostmask, identify, server, signon, idle,
channels, away)
return s
class FormatContext(object): class FormatContext(object):
def __init__(self): def __init__(self):
self.reset() self.reset()