From 3bfa148c88b8a4cb2292566c9c0c9cda4c9f7860 Mon Sep 17 00:00:00 2001 From: Mikaela Suomalainen Date: Fri, 22 Aug 2014 18:52:13 +0300 Subject: [PATCH 01/13] .mailmap: coerse my old name to my new --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index cfb26288b..9a6ee8d84 100644 --- a/.mailmap +++ b/.mailmap @@ -1 +1,2 @@ James McCoy +Mikaela Suomalainen From 5e8896fe6dc3d0d411a58450fe380ab5724ff4ad Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 17 Dec 2014 08:01:25 +0100 Subject: [PATCH 02/13] QuoteGrabs: Prevent quote grabs from queries. Signed-off-by: James McCoy --- plugins/QuoteGrabs/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/QuoteGrabs/plugin.py b/plugins/QuoteGrabs/plugin.py index 6ec8f9718..09135810f 100644 --- a/plugins/QuoteGrabs/plugin.py +++ b/plugins/QuoteGrabs/plugin.py @@ -253,7 +253,7 @@ class QuoteGrabs(callbacks.Plugin): # opposed to channel which is used to determine which db to store the # quote in chan = msg.args[0] - if chan is None: + if chan is None or not irc.isChannel(chan): raise callbacks.ArgumentError if ircutils.nickEqual(nick, msg.nick): irc.error('You can\'t quote grab yourself.', Raise=True) From 4f489fad61d323fe72851425963303ee548e0377 Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 18 Dec 2014 21:31:59 -0500 Subject: [PATCH 03/13] Move project pointers from Sourceforge to GitHub Closes Supybot/Supybot#7 Signed-off-by: James McCoy --- README | 2 +- docs/FAQ.rst | 6 +++--- plugins/Ctcp/plugin.py | 2 +- plugins/Misc/plugin.py | 2 +- plugins/RSS/README.txt | 2 +- setup.py | 6 +++--- src/conf.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README b/README index 23607adc6..3a750b86e 100644 --- a/README +++ b/README @@ -14,7 +14,7 @@ irc.freenode.net or irc.oftc.net (we have a Supybot there relaying, so either network works) and ask questions. We'll be happy to help wherever we can. And by all means, if you find anything hard to understand or think you know of a better way to do something, -*please* post it on Sourceforge.net so we can improve the bot! +*please* post it on GitHub so we can improve the bot! WINDOWS USERS: -------------- diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 62c07f887..6d08b4318 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -176,7 +176,7 @@ Why can't Supybot find the plugin I want to load? I've found a bug, what do I do? - Submit your bug on `Sourceforge`_ through our `project page`_. + Submit your bug on `GitHub`_ through our `project page`_. Is Python installed? @@ -196,6 +196,6 @@ Is Python installed? .. _website: http://supybot.com/ .. _blocks: http://freenode.net/faq.shtml#blockingmessages .. _tsocks: http://tsocks.sourceforge.net -.. _Sourceforge: http://sourceforge.net/ -.. _project page: http://sourceforge.net/projects/supybot +.. _GitHub: https://github.com/ +.. _project page: https://github.com/Supybot/Supybot .. _download it: http://python.org/download/ diff --git a/plugins/Ctcp/plugin.py b/plugins/Ctcp/plugin.py index e62404083..6396b06bf 100644 --- a/plugins/Ctcp/plugin.py +++ b/plugins/Ctcp/plugin.py @@ -105,7 +105,7 @@ class Ctcp(callbacks.PluginRegexp): "\x01SOURCE\x01" self.log.info('Received CTCP SOURCE from %s', msg.prefix) self._reply(irc, msg, - 'SOURCE http://www.sourceforge.net/projects/supybot/') + 'SOURCE https://github.com/Supybot/Supybot') def doNotice(self, irc, msg): if ircmsgs.isCtcp(msg): diff --git a/plugins/Misc/plugin.py b/plugins/Misc/plugin.py index 93fb8f645..c4c93e15a 100644 --- a/plugins/Misc/plugin.py +++ b/plugins/Misc/plugin.py @@ -227,7 +227,7 @@ class Misc(callbacks.Plugin): Returns a URL saying where to get Supybot. """ - irc.reply('My source is at http://sourceforge.net/projects/supybot/') + irc.reply('My source is at https://github.com/Supybot/Supybot') source = wrap(source) def more(self, irc, msg, args, nick): diff --git a/plugins/RSS/README.txt b/plugins/RSS/README.txt index 0d1655156..e0d454fc2 100644 --- a/plugins/RSS/README.txt +++ b/plugins/RSS/README.txt @@ -9,7 +9,7 @@ Basic usage ----------- Adding a feed -@rss add supybot http://sourceforge.net/export/rss2_projfiles.php?group_id=58965 +@rss add supybot https://github.com/Supybot/Supybot/commits/master.atom Add announcements for a feed @rss announce add supybot diff --git a/setup.py b/setup.py index 646dcfb18..cd5c34d3b 100644 --- a/setup.py +++ b/setup.py @@ -78,9 +78,9 @@ setup( author='Jeremy Fincher', author_email='jemfinch@supybot.com', maintainer='James McCoy', - maintainer_email='jamessan@users.sourceforge.net', - url='https://sourceforge.net/projects/supybot/', - download_url='https://sourceforge.net/projects/supybot/files/', + maintainer_email='vega.james@gmail.com', + url='https://github.com/Supybot/Supybot', + download_url='https://github.com/Supybot/Supybot/releases', platforms=['linux', 'linux2', 'win32', 'cygwin', 'darwin'], description='A flexible and extensible Python IRC bot and framework.', long_description=normalizeWhitespace("""A robust, full-featured Python IRC diff --git a/src/conf.py b/src/conf.py index 458b9c865..63b8cace6 100644 --- a/src/conf.py +++ b/src/conf.py @@ -525,7 +525,7 @@ registerChannelValue(supybot.replies, 'requiresPrivacy', registerChannelValue(supybot.replies, 'possibleBug', registry.NormalizedString("""This may be a bug. If you think it is, please file a bug report at - .""", + .""", """Determines what message the bot sends when it thinks you've encountered a bug that the developers don't know about.""")) ### From 2fda75e1b27235c7037606cfdbd9ec22f9172615 Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 1 Jan 2015 14:51:34 -0500 Subject: [PATCH 04/13] Allow >= 2.0 versions of python-dateutil Signed-off-by: James McCoy --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cd5c34d3b..1a14b9df2 100644 --- a/setup.py +++ b/setup.py @@ -120,7 +120,7 @@ setup( install_requires=[ # Time plugin - 'python-dateutil <2.0,>=1.3', + 'python-dateutil !=2.0,>=1.3', 'feedparser', ], From b99ff28e334f0daa1a4c6994a0f845c9a6479942 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 26 Apr 2014 12:43:45 +0000 Subject: [PATCH 05/13] utils.web.getUrl: add the 'timeout' argument (and fix the doc of getUrlFd). Signed-off-by: James McCoy --- src/utils/web.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/web.py b/src/utils/web.py index 163a937a4..f8e925af6 100644 --- a/src/utils/web.py +++ b/src/utils/web.py @@ -99,7 +99,7 @@ defaultHeaders = { proxy = None def getUrlFd(url, headers=None, data=None, timeout=None): - """getUrlFd(url, headers=None, data=None) + """getUrlFd(url, headers=None, data=None, timeout=None) Opens the given url and returns a file object. Headers and data are a dict and string, respectively, as per urllib2.Request's arguments.""" @@ -136,13 +136,13 @@ def getUrlFd(url, headers=None, data=None, timeout=None): except ValueError, e: raise Error, strError(e) -def getUrl(url, size=None, headers=None, data=None): - """getUrl(url, size=None, headers=None, data=None) +def getUrl(url, size=None, headers=None, data=None, timeout=None): + """getUrl(url, size=None, headers=None, data=None, timeout=None) Gets a page. Returns a string that is the page gotten. Size is an integer number of bytes to read from the URL. Headers and data are dicts as per urllib2.Request's arguments.""" - fd = getUrlFd(url, headers=headers, data=data) + fd = getUrlFd(url, headers=headers, data=data, timeout=timeout) try: if size is None: text = fd.read() From a8cd99f12199ab27f31779f25c4f3e12caf5290e Mon Sep 17 00:00:00 2001 From: James McCoy Date: Wed, 18 Feb 2015 21:35:57 -0500 Subject: [PATCH 06/13] commands._getRe: Restore original args for any failure String.re uses first('regexpMatcher', 'regexpReplacer'). If the args provided to String.re are not a matcher and are longer than a single IRC message (e.g., from a nested command), then regexpReplacer would never be tried. A too long error should be handled the same as running out of args while trying to find a valid regexp. Signed-off-by: James McCoy --- src/commands.py | 6 +++--- test/test_commands.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands.py b/src/commands.py index ef3d2c043..7747589a8 100644 --- a/src/commands.py +++ b/src/commands.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2009-2010, James McCoy +# Copyright (c) 2009-2010,2015, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -375,8 +375,8 @@ def _getRe(f): else: state.args.append(s) else: - state.errorInvalid('regular expression', s) - except IndexError: + raise ValueError + except (ValueError, IndexError): args[:] = original state.errorInvalid('regular expression', s) return get diff --git a/test/test_commands.py b/test/test_commands.py index 31c9a04fc..cc94c8c1f 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -1,5 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher +# Copyright (c) 2015, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -181,5 +182,9 @@ class FirstTestCase(CommandsTestCase): self.assertStateErrored([first('int', 'something')], ['words'], errored=False) + def testLongRegexp(self): + spec = [first('regexpMatcher', 'regexpReplacer'), 'text'] + self.assertStateErrored(spec, ['s/foo/bar/', 'x' * 512], errored=False) + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: From c3695c94194510cc53ec6da06dfe60fc4416430a Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 26 Mar 2015 00:11:36 -0400 Subject: [PATCH 07/13] 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 --- plugins/Network/plugin.py | 86 ++----------------------------- plugins/Relay/plugin.py | 63 ++-------------------- src/ircutils.py | 106 +++++++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 142 deletions(-) diff --git a/plugins/Network/plugin.py b/plugins/Network/plugin.py index 8ab5cdf48..cc9fbff50 100644 --- a/plugins/Network/plugin.py +++ b/plugins/Network/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010, James McCoy +# Copyright (c) 2010,2015 James McCoy # All rights reserved. # # 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: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] - hostmask = '@'.join(d['311'].args[2:4]) - user = d['311'].args[-1] - if '319' in d: - 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 = '' - signon = '' - if '312' in d: - server = d['312'].args[2] - else: - server = '' - 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) + d['318'] = msg + s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, + channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] diff --git a/plugins/Relay/plugin.py b/plugins/Relay/plugin.py index 203f8ecf3..c1bf26403 100644 --- a/plugins/Relay/plugin.py +++ b/plugins/Relay/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010, James McCoy +# Copyright (c) 2010,2015 James McCoy # All rights reserved. # # 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: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] - hostmask = '@'.join(d['311'].args[2:4]) - user = d['311'].args[-1] - if '319' in d: - 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 = '' - signon = '' - if '312' in d: - server = d['312'].args[2] - else: - server = '' - 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) + d['318'] = msg + s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, + channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] diff --git a/src/ircutils.py b/src/ircutils.py index c107a16d7..b234ed0f7 100644 --- a/src/ircutils.py +++ b/src/ircutils.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2009,2011, James McCoy +# Copyright (c) 2009,2011,2015 James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -338,6 +338,110 @@ def stripFormatting(s): s = stripUnderline(s) 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 = '' + signon = '' + if '312' in replies: + server = replies['312'].args[2] + else: + server = '' + 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): def __init__(self): self.reset() From cffbd959e8e3db90d353d28c85720d6d61ae4d44 Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 26 Mar 2015 01:33:04 -0400 Subject: [PATCH 08/13] Add handling of 437 (nick temporarily unavailable) errors Servers bind a nick to a connection for a short window after it disappears to try and avoid contention over nicks. This may cause a 437 during connection to a server (c.f. ProgVal/Limnoria#1033) or even during normal nick changes, if the timing is lucky. Add handling for this error to the startup code and the Admin plugin. Signed-off-by: James McCoy --- plugins/Admin/plugin.py | 12 +++++++++++- src/irclib.py | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/Admin/plugin.py b/plugins/Admin/plugin.py index 4f5abb728..71775a7ad 100644 --- a/plugins/Admin/plugin.py +++ b/plugins/Admin/plugin.py @@ -49,8 +49,8 @@ class Admin(callbacks.Plugin): def do437(self, irc, msg): """Nick/channel temporarily unavailable.""" target = msg.args[0] + t = time.time() + 30 if irc.isChannel(target): # We don't care about nicks. - t = time.time() + 30 # Let's schedule a rejoin. networkGroup = conf.supybot.networks.get(irc.network) def rejoin(): @@ -60,6 +60,16 @@ class Admin(callbacks.Plugin): schedule.addEvent(rejoin, t) self.log.info('Scheduling a rejoin to %s at %s; ' 'Channel temporarily unavailable.', target, t) + else: + irc = self.pendingNickChanges.get(irc, None) + if irc is not None: + def nick(): + irc.queueMsg(ircmsgs.nick(target)) + schedule.addEvent(nick, t) + self.log.info('Scheduling a nick change to %s at %s; ' + 'Nick temporarily unavailable.', target, t) + else: + self.log.debug('Got 437 without Admin.nick being called.') def do471(self, irc, msg): try: diff --git a/src/irclib.py b/src/irclib.py index 27c373c1a..41a141136 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -939,8 +939,11 @@ class Irc(IrcCommandDispatcher): if not self.afterConnect: newNick = self._getNextNick() assert newNick != self.nick - log.info('Got 433: %s %s. Trying %s.',self.nick, problem, newNick) + log.info('Got %s: %s %s. Trying %s.', + msg.command, self.nick, problem, newNick) self.sendMsg(ircmsgs.nick(newNick)) + def do437(self, msg): + self.do43x(msg, 'is temporarily unavailable') def do433(self, msg): self.do43x(msg, 'is in use') def do432(self, msg): From b0846f914e164a75cfb70cff3c47c587a3fa041b Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 3 Mar 2015 08:53:58 +0100 Subject: [PATCH 09/13] Fix crash for commands with ambiguous getopts shortcuts and no docstring. Signed-off-by: James McCoy --- src/callbacks.py | 2 +- test/test_commands.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/callbacks.py b/src/callbacks.py index b52d3e726..52c46d044 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1207,7 +1207,7 @@ class Commands(BasePlugin): utils.exnToString(e)) help = self.getCommandHelp(command) if help.endswith('command has no help.'): - irc.error('Invalid arguments for %s.' % method.__name__) + irc.error('Invalid arguments for %s.' % formatCommand(command)) else: irc.reply(help) except (SyntaxError, Error), e: diff --git a/test/test_commands.py b/test/test_commands.py index cc94c8c1f..c23d5c779 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -28,6 +28,8 @@ # POSSIBILITY OF SUCH DAMAGE. ### +import getopt + from supybot.test import * from supybot.commands import * @@ -117,6 +119,17 @@ class GeneralContextTestCase(CommandsTestCase): ['12', '--foo', 'baz', '--bar', '13', '15'], [12, [('foo', 'baz'), ('bar', 13)], 15]) + def testGetoptsShort(self): + spec = ['int', getopts({'foo': None, 'bar': 'int'}), 'int'] + self.assertState(spec, + ['12', '--f', 'baz', '--ba', '13', '15'], + [12, [('foo', 'baz'), ('bar', 13)], 15]) + + def testGetoptsConflict(self): + spec = ['int', getopts({'foo': None, 'fbar': 'int'}), 'int'] + self.assertRaises(getopt.GetoptError, self.assertStateErrored, + spec, ['12', '--f', 'baz', '--ba', '13', '15']) + def testAny(self): self.assertState([any('int')], ['1', '2', '3'], [[1, 2, 3]]) self.assertState([None, any('int')], ['1', '2', '3'], ['1', [2, 3]]) @@ -186,5 +199,19 @@ class FirstTestCase(CommandsTestCase): spec = [first('regexpMatcher', 'regexpReplacer'), 'text'] self.assertStateErrored(spec, ['s/foo/bar/', 'x' * 512], errored=False) +class GetoptTestCase(PluginTestCase): + plugins = ('Misc',) # We put something so it does not complain + class Foo(callbacks.Plugin): + def bar(self, irc, msg, args, optlist): + irc.reply(' '.join(sorted(['%s:%d'%x for x in optlist]))) + bar = wrap(bar, [getopts({'foo': 'int', 'fbar': 'int'})]) + + def testGetoptsExact(self): + self.irc.addCallback(self.Foo(self.irc)) + self.assertResponse('bar --foo 3 --fbar 4', 'fbar:4 foo:3') + self.assertResponse('bar --fo 3 --fb 4', 'fbar:4 foo:3') + self.assertResponse('bar --f 3 --fb 5', + 'Error: Invalid arguments for bar.') + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: From 6a134eb30278a90ba861d658c5cfbeb55da85b02 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 3 Mar 2015 08:55:00 +0100 Subject: [PATCH 10/13] Update string comparison to a newer string. Signed-off-by: James McCoy --- src/callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/callbacks.py b/src/callbacks.py index 52c46d044..80db2300f 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1206,7 +1206,7 @@ class Commands(BasePlugin): self.log.debug('Got %s, giving argument error.', utils.exnToString(e)) help = self.getCommandHelp(command) - if help.endswith('command has no help.'): + if 'command has no help.' in help: irc.error('Invalid arguments for %s.' % formatCommand(command)) else: irc.reply(help) From 8266870d9f41f4a76f6c1ef0eb01122a11623efe Mon Sep 17 00:00:00 2001 From: nyuszika7h Date: Sat, 24 Jan 2015 19:33:33 +0100 Subject: [PATCH 11/13] ShrinkUrl: Remove xrl.us > Please note: Adding new links has been disabled since September 2014 > after 14 months notice. Source: http://metamark.net/ Signed-off-by: James McCoy --- plugins/ShrinkUrl/config.py | 6 +++--- plugins/ShrinkUrl/plugin.py | 27 --------------------------- plugins/ShrinkUrl/test.py | 5 ----- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/plugins/ShrinkUrl/config.py b/plugins/ShrinkUrl/config.py index bb9c7338d..8229106f5 100644 --- a/plugins/ShrinkUrl/config.py +++ b/plugins/ShrinkUrl/config.py @@ -40,11 +40,11 @@ def configure(advanced): conf.supybot.plugins.ShrinkUrl.shrinkSnarfer.setValue(True) class ShrinkService(registry.OnlySomeStrings): - """Valid values include 'ln', 'tiny', 'xrl', 'goo', 'ur1', and 'x0'.""" - validStrings = ('ln', 'tiny', 'xrl', 'goo', 'ur1', 'x0') + """Valid values include 'ln', 'tiny', 'goo', 'ur1', and 'x0'.""" + validStrings = ('ln', 'tiny', 'goo', 'ur1', 'x0') class ShrinkCycle(registry.SpaceSeparatedListOfStrings): - """Valid values include 'ln', 'tiny', 'xrl', 'goo', 'ur1', and 'x0'.""" + """Valid values include 'ln', 'tiny', 'goo', 'ur1', and 'x0'.""" Value = ShrinkService def __init__(self, *args, **kwargs): diff --git a/plugins/ShrinkUrl/plugin.py b/plugins/ShrinkUrl/plugin.py index dce4575dd..17903d6a7 100644 --- a/plugins/ShrinkUrl/plugin.py +++ b/plugins/ShrinkUrl/plugin.py @@ -201,33 +201,6 @@ class ShrinkUrl(callbacks.PluginRegexp): irc.errorPossibleBug(str(e)) tiny = thread(wrap(tiny, ['url'])) - _xrlApi = 'http://metamark.net/api/rest/simple' - def _getXrlUrl(self, url): - quotedurl = utils.web.urlquote(url) - try: - return self.db.get('xrl', quotedurl) - except KeyError: - data = utils.web.urlencode({'long_url': url}) - text = utils.web.getUrl(self._xrlApi, data=data) - if text.startswith('ERROR:'): - raise ShrinkError, text[6:] - self.db.set('xrl', quotedurl, text) - return text - - def xrl(self, irc, msg, args, url): - """ - - Returns an xrl.us version of . - """ - try: - xrlurl = self._getXrlUrl(url) - m = irc.reply(xrlurl) - if m is not None: - m.tag('shrunken') - except ShrinkError, e: - irc.error(str(e)) - xrl = thread(wrap(xrl, ['url'])) - _gooApi = 'https://www.googleapis.com/urlshortener/v1/url' def _getGooUrl(self, url): url = utils.web.urlquote(url) diff --git a/plugins/ShrinkUrl/test.py b/plugins/ShrinkUrl/test.py index 1b3a7aa40..7f1c0208e 100644 --- a/plugins/ShrinkUrl/test.py +++ b/plugins/ShrinkUrl/test.py @@ -41,8 +41,6 @@ class ShrinkUrlTestCase(ChannelPluginTestCase): (udUrl, r'http://tinyurl.com/u479')], 'ln': [(sfUrl, r'http://ln-s.net/4LVF'), (udUrl, r'http://ln-s.net/2\$K')], - 'xrl': [(sfUrl, r'http://xrl.us/bfq8ik'), - (udUrl, r'http://xrl.us/bfnyji')], 'goo': [(sfUrl, r'http://goo.gl/3c59N'), (udUrl, r'http://goo.gl/ocTga')], 'ur1': [(sfUrl, r'http://ur1.ca/9xl25'), @@ -98,9 +96,6 @@ class ShrinkUrlTestCase(ChannelPluginTestCase): def testLnsnarf(self): self._snarf('ln') - def testXrlsnarf(self): - self._snarf('xrl') - def testGoosnarf(self): self._snarf('goo') From 01e776edc2b99adb6bdebc4cc2205d36e726ff65 Mon Sep 17 00:00:00 2001 From: Mikaela Suomalainen Date: Fri, 9 May 2014 14:37:47 +0300 Subject: [PATCH 12/13] ShrinkUrl: use x0 by default. Fixes #617. x0 has the smallest working output. ur1 had the second smallest. Their difference is one character. Signed-off-by: James McCoy --- plugins/ShrinkUrl/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ShrinkUrl/config.py b/plugins/ShrinkUrl/config.py index 8229106f5..2b45e657c 100644 --- a/plugins/ShrinkUrl/config.py +++ b/plugins/ShrinkUrl/config.py @@ -87,7 +87,7 @@ conf.registerChannelValue(ShrinkUrl, 'outFilter', of outgoing messages if those URLs are longer than supybot.plugins.ShrinkUrl.minimumLength.""")) conf.registerChannelValue(ShrinkUrl, 'default', - ShrinkService('ln', """Determines what website the bot will use when + ShrinkService('x0', """Determines what website the bot will use when shrinking a URL.""")) conf.registerGlobalValue(ShrinkUrl, 'bold', registry.Boolean(True, """Determines whether this plugin will bold certain From 159c1e7cd886cb5f8c68d1ba703b1d302a716e5d Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 28 Feb 2015 09:52:42 -0800 Subject: [PATCH 13/13] ShrinkUrl: Remove ln (Closes #618). ln-s.net has been dead for over half a year now; both their website homepage and any previous shortened links return an HTTP 503. Thus, it isn't very useful to leave this broken command lingering around... Signed-off-by: James McCoy --- plugins/ShrinkUrl/config.py | 6 +++--- plugins/ShrinkUrl/plugin.py | 28 ---------------------------- plugins/ShrinkUrl/test.py | 17 +++++------------ 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/plugins/ShrinkUrl/config.py b/plugins/ShrinkUrl/config.py index 2b45e657c..b6aa88d3a 100644 --- a/plugins/ShrinkUrl/config.py +++ b/plugins/ShrinkUrl/config.py @@ -40,8 +40,8 @@ def configure(advanced): conf.supybot.plugins.ShrinkUrl.shrinkSnarfer.setValue(True) class ShrinkService(registry.OnlySomeStrings): - """Valid values include 'ln', 'tiny', 'goo', 'ur1', and 'x0'.""" - validStrings = ('ln', 'tiny', 'goo', 'ur1', 'x0') + """Valid values include 'tiny', 'goo', 'ur1', and 'x0'.""" + validStrings = ('tiny', 'goo', 'ur1', 'x0') class ShrinkCycle(registry.SpaceSeparatedListOfStrings): """Valid values include 'ln', 'tiny', 'goo', 'ur1', and 'x0'.""" @@ -70,7 +70,7 @@ conf.registerChannelValue(ShrinkUrl, 'shrinkSnarfer', shrink snarfer is enabled. This snarfer will watch for URLs in the channel, and if they're sufficiently long (as determined by supybot.plugins.ShrinkUrl.minimumLength) it will post a - smaller URL from either ln-s.net or tinyurl.com, as denoted in + smaller URL from the service denoted in supybot.plugins.ShrinkUrl.default.""")) conf.registerChannelValue(ShrinkUrl.shrinkSnarfer, 'showDomain', registry.Boolean(True, """Determines whether the snarfer will show the diff --git a/plugins/ShrinkUrl/plugin.py b/plugins/ShrinkUrl/plugin.py index 17903d6a7..68810afac 100644 --- a/plugins/ShrinkUrl/plugin.py +++ b/plugins/ShrinkUrl/plugin.py @@ -149,34 +149,6 @@ class ShrinkUrl(callbacks.PluginRegexp): shrinkSnarfer = urlSnarfer(shrinkSnarfer) shrinkSnarfer.__doc__ = utils.web._httpUrlRe - def _getLnUrl(self, url): - url = utils.web.urlquote(url) - try: - return self.db.get('ln', url) - except KeyError: - text = utils.web.getUrl('http://ln-s.net/home/api.jsp?url=' + url) - (code, text) = text.split(None, 1) - text = text.strip() - if code == '200': - self.db.set('ln', url, text) - return text - else: - raise ShrinkError, text - - def ln(self, irc, msg, args, url): - """ - - Returns an ln-s.net version of . - """ - try: - lnurl = self._getLnUrl(url) - m = irc.reply(lnurl) - if m is not None: - m.tag('shrunken') - except ShrinkError, e: - irc.error(str(e)) - ln = thread(wrap(ln, ['url'])) - def _getTinyUrl(self, url): try: return self.db.get('tiny', url) diff --git a/plugins/ShrinkUrl/test.py b/plugins/ShrinkUrl/test.py index 7f1c0208e..3bd9535e8 100644 --- a/plugins/ShrinkUrl/test.py +++ b/plugins/ShrinkUrl/test.py @@ -39,8 +39,6 @@ class ShrinkUrlTestCase(ChannelPluginTestCase): 'term=all+your+base+are+belong+to+us' tests = {'tiny': [(sfUrl, r'http://tinyurl.com/yg8r28z'), (udUrl, r'http://tinyurl.com/u479')], - 'ln': [(sfUrl, r'http://ln-s.net/4LVF'), - (udUrl, r'http://ln-s.net/2\$K')], 'goo': [(sfUrl, r'http://goo.gl/3c59N'), (udUrl, r'http://goo.gl/ocTga')], 'ur1': [(sfUrl, r'http://ur1.ca/9xl25'), @@ -62,16 +60,14 @@ class ShrinkUrlTestCase(ChannelPluginTestCase): origsnarfer = snarfer() try: self.assertNotError( - 'config plugins.ShrinkUrl.serviceRotation ln x0') + 'config plugins.ShrinkUrl.serviceRotation goo x0') self.assertError( - 'config plugins.ShrinkUrl.serviceRotation ln x1') + 'config plugins.ShrinkUrl.serviceRotation goo x1') snarfer.setValue(True) - self.assertSnarfRegexp(self.udUrl, r'%s.* \(at' % - self.tests['ln'][1][1]) - self.assertSnarfRegexp(self.udUrl, r'%s.* \(at' % + self.assertSnarfRegexp(self.udUrl, r'.*%s.* \(at' % + self.tests['goo'][1][1]) + self.assertSnarfRegexp(self.udUrl, r'.*%s.* \(at' % self.tests['x0'][1][1]) - self.assertSnarfRegexp(self.udUrl, r'%s.* \(at' % - self.tests['ln'][1][1]) finally: cycle.setValue(origcycle) snarfer.setValue(origsnarfer) @@ -93,9 +89,6 @@ class ShrinkUrlTestCase(ChannelPluginTestCase): def testTinysnarf(self): self._snarf('tiny') - def testLnsnarf(self): - self._snarf('ln') - def testGoosnarf(self): self._snarf('goo')