Merge branch 'maint/0.83.4'

This commit is contained in:
James Vega 2010-09-08 23:50:35 -04:00
commit 0abe22f5d1
10 changed files with 34 additions and 158 deletions

View File

@ -1,5 +1,6 @@
### ###
# Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2003-2005, Jeremiah Fincher
# Copyright (c) 2010, James Vega
# 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
@ -57,11 +58,7 @@ class Games(callbacks.Plugin):
ten-sided dice. ten-sided dice.
""" """
(dice, sides) = utils.iter.imap(int, m.groups()) (dice, sides) = utils.iter.imap(int, m.groups())
if dice > 6: if sides < 3:
irc.error('You can\'t roll more than 6 dice.')
elif sides > 100:
irc.error('Dice can\'t have more than 100 sides.')
elif sides < 3:
irc.error('Dice can\'t have fewer than 3 sides.') irc.error('Dice can\'t have fewer than 3 sides.')
else: else:
L = [0] * dice L = [0] * dice

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2005, Jeremiah Fincher
# Copyright (c) 2008-2009, James Vega # Copyright (c) 2008-2010, James Vega
# 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
@ -37,13 +37,7 @@ def configure(advanced):
output("""The Google plugin has the functionality to watch for URLs output("""The Google plugin has the functionality to watch for URLs
that match a specific pattern. (We call this a snarfer) that match a specific pattern. (We call this a snarfer)
When supybot sees such a URL, it will parse the web page When supybot sees such a URL, it will parse the web page
for information and reply with the results. for information and reply with the results.""")
Google has two available snarfers: Google Groups link
snarfing and a google search snarfer.""")
if yn('Do you want the Google Groups link snarfer enabled by '
'default?'):
conf.supybot.plugins.Google.groupsSnarfer.setValue(True)
if yn('Do you want the Google search snarfer enabled by default?'): if yn('Do you want the Google search snarfer enabled by default?'):
conf.supybot.plugins.Google.searchSnarfer.setValue(True) conf.supybot.plugins.Google.searchSnarfer.setValue(True)
@ -104,10 +98,6 @@ conf.registerGlobalValue(Google, 'referer',
the Referer field of the search requests. If this value is empty, a the Referer field of the search requests. If this value is empty, a
Referer will be generated in the following format: Referer will be generated in the following format:
http://$server/$botName""")) http://$server/$botName"""))
conf.registerChannelValue(Google, 'groupsSnarfer',
registry.Boolean(False, """Determines whether the groups snarfer is
enabled. If so, URLs at groups.google.com will be snarfed and their
group/title messaged to the channel."""))
conf.registerChannelValue(Google, 'searchSnarfer', conf.registerChannelValue(Google, 'searchSnarfer',
registry.Boolean(False, """Determines whether the search snarfer is registry.Boolean(False, """Determines whether the search snarfer is
enabled. If so, messages (even unaddressed ones) beginning with the word enabled. If so, messages (even unaddressed ones) beginning with the word

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2002-2004, Jeremiah Fincher
# Copyright (c) 2008-2009, James Vega # Copyright (c) 2008-2010, James Vega
# 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
@ -66,7 +66,7 @@ except ImportError:
class Google(callbacks.PluginRegexp): class Google(callbacks.PluginRegexp):
threaded = True threaded = True
callBefore = ['Web'] callBefore = ['Web']
regexps = ['googleSnarfer', 'googleGroups'] regexps = ['googleSnarfer']
_colorGoogles = {} _colorGoogles = {}
def _getColorGoogle(self, m): def _getColorGoogle(self, m):
@ -288,44 +288,6 @@ class Google(callbacks.PluginRegexp):
irc.reply(url.encode('utf-8'), prefixNick=False) irc.reply(url.encode('utf-8'), prefixNick=False)
googleSnarfer = urlSnarfer(googleSnarfer) googleSnarfer = urlSnarfer(googleSnarfer)
_ggThread = re.compile(r'Subject: <b>([^<]+)</b>', re.I)
_ggGroup = re.compile(r'<TITLE>Google Groups :\s*([^<]+)</TITLE>', re.I)
_ggThreadm = re.compile(r'src="(/group[^"]+)">', re.I)
_ggSelm = re.compile(r'selm=[^&]+', re.I)
_threadmThread = re.compile(r'TITLE="([^"]+)">', re.I)
_threadmGroup = re.compile(r'class=groupname[^>]+>([^<]+)<', re.I)
def googleGroups(self, irc, msg, match):
r"http://groups.google.[\w.]+/\S+\?(\S+)"
if not self.registryValue('groupsSnarfer', msg.args[0]):
return
queries = cgi.parse_qsl(match.group(1))
queries = [q for q in queries if q[0] in ('threadm', 'selm')]
if not queries:
return
queries.append(('hl', 'en'))
url = 'http://groups.google.com/groups?' + urllib.urlencode(queries)
text = utils.web.getUrl(url)
mThread = None
mGroup = None
if 'threadm=' in url:
path = self._ggThreadm.search(text)
if path is not None:
url = 'http://groups-beta.google.com' + path.group(1)
text = utils.web.getUrl(url)
mThread = self._threadmThread.search(text)
mGroup = self._threadmGroup.search(text)
else:
mThread = self._ggThread.search(text)
mGroup = self._ggGroup.search(text)
if mThread and mGroup:
irc.reply(format('Google Groups: %s, %s',
mGroup.group(1), mThread.group(1)),
prefixNick=False)
else:
self.log.debug('Unable to snarf. %s doesn\'t appear to be a '
'proper Google Groups page.', match.group(1))
googleGroups = urlSnarfer(googleGroups)
def _googleUrl(self, s): def _googleUrl(self, s):
s = s.replace('+', '%2B') s = s.replace('+', '%2B')
s = s.replace(' ', '+') s = s.replace(' ', '+')

View File

@ -64,60 +64,4 @@ class GoogleTestCase(ChannelPluginTestCase):
def testCalcDoesNotHaveExtraSpaces(self): def testCalcDoesNotHaveExtraSpaces(self):
self.assertNotRegexp('google calc 1000^2', r'\s+,\s+') self.assertNotRegexp('google calc 1000^2', r'\s+,\s+')
def testGroupsSnarfer(self):
orig = conf.supybot.plugins.Google.groupsSnarfer()
try:
conf.supybot.plugins.Google.groupsSnarfer.setValue(True)
# This should work, and does work in practice, but is failing
# in the tests.
#self.assertSnarfRegexp(
# 'http://groups.google.com/groups?dq=&hl=en&lr=lang_en&'
# 'ie=UTF-8&oe=UTF-8&selm=698f09f8.0310132012.738e22fc'
# '%40posting.google.com',
# r'comp\.lang\.python.*question: usage of __slots__')
self.assertSnarfRegexp(
'http://groups.google.com/groups?selm=ExDm.8bj.23'
'%40gated-at.bofh.it&oe=UTF-8&output=gplain',
r'linux\.kernel.*NFS client freezes')
self.assertSnarfRegexp(
'http://groups.google.com/groups?q=kernel+hot-pants&'
'hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=1.5.4.32.199703131'
'70853.00674d60%40adan.kingston.net&rnum=1',
r'Madrid Bluegrass Ramble')
self.assertSnarfRegexp(
'http://groups.google.com/groups?selm=1.5.4.32.19970'
'313170853.00674d60%40adan.kingston.net&oe=UTF-8&'
'output=gplain',
r'Madrid Bluegrass Ramble')
self.assertSnarfRegexp(
'http://groups.google.com/groups?dq=&hl=en&lr=&'
'ie=UTF-8&threadm=mailman.1010.1069645289.702.'
'python-list%40python.org&prev=/groups%3Fhl%3Den'
'%26lr%3D%26ie%3DUTF-8%26group%3Dcomp.lang.python',
r'comp\.lang\.python.*What exactly are bound')
# Test for Bug #1002547
self.assertSnarfRegexp(
'http://groups.google.com/groups?q=supybot+is+the&'
'hl=en&lr=&ie=UTF-8&c2coff=1&selm=1028329672'
'%40freshmeat.net&rnum=9',
r'fm\.announce.*SupyBot')
finally:
conf.supybot.plugins.Google.groupsSnarfer.setValue(orig)
def testConfig(self):
orig = conf.supybot.plugins.Google.groupsSnarfer()
try:
conf.supybot.plugins.Google.groupsSnarfer.setValue(False)
self.assertSnarfNoResponse(
'http://groups.google.com/groups?dq=&hl=en&lr=lang_en&'
'ie=UTF-8&oe=UTF-8&selm=698f09f8.0310132012.738e22fc'
'%40posting.google.com')
conf.supybot.plugins.Google.groupsSnarfer.setValue(True)
self.assertSnarfNotError(
'http://groups.google.com/groups?dq=&hl=en&lr=lang_en&'
'ie=UTF-8&oe=UTF-8&selm=698f09f8.0310132012.738e22fc'
'%40posting.google.com')
finally:
conf.supybot.plugins.Google.groupsSnarfer.setValue(orig)
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -276,11 +276,12 @@ class Karma(callbacks.Plugin):
def karma(self, irc, msg, args, channel, things): def karma(self, irc, msg, args, channel, things):
"""[<channel>] [<thing> ...] """[<channel>] [<thing> ...]
Returns the karma of <text>. If <thing> is not given, returns the top Returns the karma of <thing>. If <thing> is not given, returns the top
three and bottom three karmas. If one <thing> is given, returns the N karmas, where N is determined by the config variable
details of its karma; if more than one <thing> is given, returns the supybot.plugins.Karma.rankingDisplay. If one <thing> is given, returns
total karma of each of the the things. <channel> is only necessary if the details of its karma; if more than one <thing> is given, returns
the message isn't sent on the channel itself. the total karma of each of the the things. <channel> is only necessary
if the message isn't sent on the channel itself.
""" """
if len(things) == 1: if len(things) == 1:
name = things[0] name = things[0]

View File

@ -47,7 +47,7 @@ class LimiterTestCase(ChannelPluginTestCase):
conf.supybot.plugins.Limiter.maximumExcess.setValue(7) conf.supybot.plugins.Limiter.maximumExcess.setValue(7)
self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host')) self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host'))
m = self.irc.takeMsg() m = self.irc.takeMsg()
self.assertEqual(m, ircmsgs.limit('#foo', 1-5)) self.assertEqual(m, ircmsgs.limit('#foo', 1+5))
finally: finally:
conf.supybot.plugins.Limiter.minimumExcess.setValue(origMin) conf.supybot.plugins.Limiter.minimumExcess.setValue(origMin)
conf.supybot.plugins.Limiter.maximumExcess.setValue(origMax) conf.supybot.plugins.Limiter.maximumExcess.setValue(origMax)

View File

@ -147,16 +147,11 @@ class News(callbacks.Plugin):
irc.reply(format('No news for %s.', channel)) irc.reply(format('No news for %s.', channel))
else: else:
try: try:
if id < 1:
raise ValueError
record = self.db.get(channel, id) record = self.db.get(channel, id)
irc.reply(str(record)) irc.reply(str(record))
except dbi.NoRecordError, id: except dbi.NoRecordError, id:
irc.errorInvalid('news item id', id) irc.errorInvalid('news item id', id)
except ValueError: news = wrap(news, ['channeldb', additional('positiveInt')])
irc.errorInvalid('news item id', id,
'<id> must be a positive integer.')
news = wrap(news, ['channeldb', additional('int')])
def remove(self, irc, msg, args, channel, id): def remove(self, irc, msg, args, channel, id):
"""[<channel>] <id> """[<channel>] <id>
@ -165,16 +160,11 @@ class News(callbacks.Plugin):
necessary if the message isn't sent in the channel itself. necessary if the message isn't sent in the channel itself.
""" """
try: try:
if id < 1:
raise ValueError
self.db.remove(channel, id) self.db.remove(channel, id)
irc.replySuccess() irc.replySuccess()
except dbi.NoRecordError: except dbi.NoRecordError:
irc.errorInvalid('news item id', id) irc.errorInvalid('news item id', id)
except ValueError: remove = wrap(remove, ['channeldb', 'positiveInt'])
irc.errorInvalid('news item id', id,
'<id> must be a positive integer.')
remove = wrap(remove, ['channeldb', 'int'])
def change(self, irc, msg, args, channel, id, replacer): def change(self, irc, msg, args, channel, id, replacer):
"""[<channel>] <id> <regexp> """[<channel>] <id> <regexp>
@ -185,16 +175,11 @@ class News(callbacks.Plugin):
isn't sent on the channel itself. isn't sent on the channel itself.
""" """
try: try:
if id < 1:
raise ValueError
self.db.change(channel, id, replacer) self.db.change(channel, id, replacer)
irc.replySuccess() irc.replySuccess()
except dbi.NoRecordError: except dbi.NoRecordError:
irc.errorInvalid('news item id', id) irc.errorInvalid('news item id', id)
except ValueError: change = wrap(change, ['channeldb', 'positiveInt', 'regexpReplacer'])
irc.errorInvalid('news item id', id,
'<id> must be a positive integer.')
change = wrap(change, ['channeldb', 'int', 'regexpReplacer'])
def old(self, irc, msg, args, channel, id): def old(self, irc, msg, args, channel, id):
"""[<channel>] [<id>] """[<channel>] [<id>]
@ -205,15 +190,10 @@ class News(callbacks.Plugin):
""" """
if id: if id:
try: try:
if id < 1:
raise ValueError
record = self.db.getOld(channel, id) record = self.db.getOld(channel, id)
irc.reply(str(record)) irc.reply(str(record))
except dbi.NoRecordError, id: except dbi.NoRecordError, id:
irc.errorInvalid('news item id', id) irc.errorInvalid('news item id', id)
except ValueError:
irc.errorInvalid('news item id', id,
'<id> must be a positive integer.')
else: else:
try: try:
records = self.db.getOld(channel) records = self.db.getOld(channel)
@ -222,7 +202,7 @@ class News(callbacks.Plugin):
irc.reply(s) irc.reply(s)
except dbi.NoRecordError: except dbi.NoRecordError:
irc.reply(format('No old news for %s.', channel)) irc.reply(format('No old news for %s.', channel))
old = wrap(old, ['channeldb', additional('int')]) old = wrap(old, ['channeldb', additional('positiveInt')])
Class = News Class = News

View File

@ -30,7 +30,6 @@
import re import re
import time import time
import fnmatch
import operator import operator
import supybot.dbi as dbi import supybot.dbi as dbi
@ -98,15 +97,9 @@ class DbiNoteDB(dbi.DB):
self.set(id, n) self.set(id, n)
def getUnnotifiedIds(self, to): def getUnnotifiedIds(self, to):
## def p(note):
## return not note.notified and note.to == to
## return [note.id for note in self.select(p)]
return self.unNotified.get(to, []) return self.unNotified.get(to, [])
def getUnreadIds(self, to): def getUnreadIds(self, to):
## def p(note):
## return not note.read and note.to == to
## return [note.id for note in self.select(p)]
return self.unRead.get(to, []) return self.unRead.get(to, [])
def send(self, frm, to, public, text): def send(self, frm, to, public, text):
@ -303,9 +296,8 @@ class Note(callbacks.Plugin):
elif option == 'sent': elif option == 'sent':
own = frm own = frm
if glob: if glob:
glob = fnmatch.translate(glob) glob = utils.python.glob2re(glob)
# ignore the trailing $ fnmatch.translate adds to the regexp criteria.append(re.compile(glob).search)
criteria.append(re.compile(glob[:-1]).search)
def match(note): def match(note):
for p in criteria: for p in criteria:
if not p(note.text): if not p(note.text):

View File

@ -1,5 +1,6 @@
### ###
# Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2003-2005, Daniel DiPaolo
# Copyright (c) 2010, James Vega
# 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
@ -30,7 +31,6 @@
import os import os
import re import re
import time import time
import fnmatch
import operator import operator
import supybot.dbi as dbi import supybot.dbi as dbi
@ -230,8 +230,8 @@ class Todo(callbacks.Plugin):
if option == 'regexp': if option == 'regexp':
criteria.append(arg.search) criteria.append(arg.search)
for glob in globs: for glob in globs:
glob = fnmatch.translate(glob) glob = utils.python.glob2re(glob)
criteria.append(re.compile(glob[:-1]).search) criteria.append(re.compile(glob).search)
try: try:
tasks = self.db.select(user.id, criteria) tasks = self.db.select(user.id, criteria)
L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks] L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks]

View File

@ -1,6 +1,6 @@
### ###
# Copyright (c) 2005-2009, Jeremiah Fincher # Copyright (c) 2005-2009, Jeremiah Fincher
# Copyright (c) 2009, James Vega # Copyright (c) 2009-2010, James Vega
# 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
@ -30,6 +30,7 @@
import sys import sys
import types import types
import fnmatch
import UserDict import UserDict
import threading import threading
@ -65,7 +66,6 @@ def changeFunctionName(f, name, doc=None):
class Object(object): class Object(object):
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
class Synchronized(type): class Synchronized(type):
METHODS = '__synchronized__' METHODS = '__synchronized__'
@ -103,5 +103,15 @@ class Synchronized(type):
newclass = super(Synchronized, cls).__new__(cls, name, bases, dict) newclass = super(Synchronized, cls).__new__(cls, name, bases, dict)
return newclass return newclass
# Translate glob to regular expression, trimming the "match EOL" portion of
# the regular expression.
if sys.version_info < (2, 6, 0):
# Pre-2.6 just uses the $ anchor
def glob2re(g):
return fnmatch.translate(g)[:-1]
else:
# Post-2.6 uses \Z(?ms) per http://issues.python.org/6665
def glob2re(g):
return fnmatch.translate(g)[:-7]
# vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: