From 5ffcf6511a70bdab15b84217d9aa31fb2dae7f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eli=C3=A1n=20Hanisch?= Date: Mon, 25 Jun 2012 19:30:11 -0300 Subject: [PATCH 01/40] some refactoing. * added Ban.type property * PersistentCache renamed to ReviewStore and made it work more like defaultdict. --- Bantracker/plugin.py | 77 +++++++++++++++++++++++++------------------- Bantracker/test.py | 2 +- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index c5644fd..052197c 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -130,6 +130,7 @@ class MsgQueue(object): queue = MsgQueue() + class Ban(object): """Hold my bans""" def __init__(self, args=None, **kwargs): @@ -171,17 +172,31 @@ class Ban(object): def time(self): return datetime.datetime.fromtimestamp(self.when) -def guessBanType(mask): - if mask[0] == '%': - return 'quiet' - elif ircutils.isUserHostmask(mask) or mask.endswith('(realname)'): - return 'ban' - return 'removal' + @property + def type(self): + mask = self.mask + if mask[0] == '%': + return 'quiet' + elif ircutils.isUserHostmask(mask) or mask.endswith('(realname)'): + if not ('*' in mask or '?' in mask or '$' in mask): + # XXX hack over hack, we are supposing these are marks as normal + # bans aren't usually set to exact match, while marks are. + return 'mark' + return 'ban' + return 'removal' -class PersistentCache(dict): + +class ReviewStore(dict): def __init__(self, filename): self.filename = conf.supybot.directories.data.dirize(filename) - self.time = 0 + self.lastReview = 0 + + def __getitem__(self, k): + try: + return dict.__getitem__(self, k) + except KeyError: + self[k] = L = [] + return L def open(self): import csv @@ -189,7 +204,7 @@ class PersistentCache(dict): reader = csv.reader(open(self.filename, 'rb')) except IOError: return - self.time = int(reader.next()[1]) + self.lastReview = int(reader.next()[1]) for row in reader: host, value = self.deserialize(*row) try: @@ -205,7 +220,7 @@ class PersistentCache(dict): writer = csv.writer(open(self.filename, 'wb')) except IOError: return - writer.writerow(('time', str(int(self.time)))) + writer.writerow(('time', str(int(self.lastReview)))) for host, values in self.iteritems(): for v in values: writer.writerow(self.serialize(host, v)) @@ -254,7 +269,7 @@ class Bantracker(callbacks.Plugin): self.db = None self.get_bans(irc) self.get_nicks(irc) - self.pendingReviews = PersistentCache('bt.reviews.db') + self.pendingReviews = ReviewStore('bt.reviews.db') self.pendingReviews.open() self._banreviewfix() # add scheduled event for check bans that need review, check every hour @@ -464,7 +479,7 @@ class Bantracker(callbacks.Plugin): return # check the type of the action taken mask = ban.mask - type = guessBanType(mask) + type = ban.type if type == 'quiet': mask = mask[1:] # check if type is enabled @@ -479,11 +494,12 @@ class Bantracker(callbacks.Plugin): if nickMatch(nick, self.registryValue('request.ignore', channel)): return if nickMatch(nick, self.registryValue('request.forward', channel)): + # somebody else should comment this (like with bans set by bots) s = "Please somebody comment on the %s of %s in %s done by %s, use:"\ " %scomment %s " %(type, mask, channel, nick, prefix, ban.id) self._sendForward(irc, s, 'request', channel) else: - # send to op + # send to operator s = "Please comment on the %s of %s in %s, use: %scomment %s " \ %(type, mask, channel, prefix, ban.id) irc.reply(s, to=nick, private=True) @@ -494,11 +510,11 @@ class Bantracker(callbacks.Plugin): # time is zero, do nothing return now = time.mktime(time.gmtime()) - lastreview = self.pendingReviews.time - self.pendingReviews.time = now # update last time reviewed - if not lastreview: + lastReview = self.pendingReviews.lastReview + self.pendingReviews.lastReview = now # update last time reviewed + if not lastReview: # initialize last time reviewed timestamp - lastreview = now - reviewTime + lastReview = now - reviewTime for channel, bans in self.bans.iteritems(): if not self.registryValue('enabled', channel) \ @@ -510,15 +526,14 @@ class Bantracker(callbacks.Plugin): # the less I touch it the better. if ban.mask.endswith('$#ubuntu-read-topic'): continue - type = guessBanType(ban.mask) - if type == 'removal': - # skip kicks - continue - if not ('*' in ban.mask or '?' in ban.mask or '$' in ban.mask): - # XXX hack over hack, we are supposing these are marks. + + type = ban.type + if type in ('removal', 'mark'): + # skip kicks and marks continue + banAge = now - ban.when - reviewWindow = lastreview - ban.when + reviewWindow = lastReview - ban.when #self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)', channel, ban.mask, # ban.who, reviewWindow, reviewTime, banAge, reviewTime - reviewWindow) if reviewWindow <= reviewTime < banAge: @@ -563,11 +578,8 @@ class Bantracker(callbacks.Plugin): self.registryValue('bansite'), ban.id) msg = ircmsgs.privmsg(nick, s) - if host in self.pendingReviews \ - and (nick, msg) not in self.pendingReviews[host]: + if (nick, msg) not in self.pendingReviews[host]: self.pendingReviews[host].append((nick, msg)) - else: - self.pendingReviews[host] = [(nick, msg)] elif banAge < reviewTime: # since we made sure bans are sorted by time, the bans left are more recent break @@ -596,10 +608,7 @@ class Bantracker(callbacks.Plugin): self.pendingReviews.clear() for host, nick, msg in nodups: - if host in self.pendingReviews: - self.pendingReviews[host].append((nick, msg)) - else: - self.pendingReviews[host] = [(nick, msg)] + self.pendingReviews[host].append((nick, msg)) def _sendReviews(self, irc, msg): host = ircutils.hostFromHostmask(msg.prefix) @@ -1225,9 +1234,9 @@ class Bantracker(callbacks.Plugin): nick, host = key.split('@', 1) else: nick, host = key, None - try: + if host in self.pendingReviews: reviews = self.pendingReviews[host] - except KeyError: + else: irc.reply('No reviews for %s, use --verbose for check the correct nick@host key.' % key) return diff --git a/Bantracker/test.py b/Bantracker/test.py index c93999a..e9c56c9 100644 --- a/Bantracker/test.py +++ b/Bantracker/test.py @@ -286,7 +286,7 @@ class BantrackerTestCase(ChannelPluginTestCase): # check not pending anymore self.assertFalse(cb.pendingReviews) - def testPersistentCache(self): + def testReviewStore(self): """Save pending reviews and when bans were last checked. This is needed for plugin reloads""" msg1 = ircmsgs.privmsg('nick', 'Hello World') From a08559a7589faf5eaecfe2b99f5d29f5d24665c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eli=C3=A1n=20Hanisch?= Date: Mon, 25 Jun 2012 19:35:42 -0300 Subject: [PATCH 02/40] Fix spurious testcase fail, sometimes seconds didn't match. --- Bantracker/plugin.py | 13 ++++++++++--- Bantracker/test.py | 13 +++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index 052197c..3b80b08 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -70,6 +70,11 @@ tz = 'UTC' def now(): return cPickle.dumps(datetime.datetime.now(pytz.timezone(tz))) +def nowSeconds(): + # apparently time.time() isn't the same thing. + # return time.time() + return int(time.mktime(time.gmtime())) + def fromTime(x): return cPickle.dumps(datetime.datetime(*time.gmtime(x)[:6], **{'tzinfo': pytz.timezone("UTC")})) @@ -509,7 +514,8 @@ class Bantracker(callbacks.Plugin): if not reviewTime: # time is zero, do nothing return - now = time.mktime(time.gmtime()) + + now = nowSeconds() lastReview = self.pendingReviews.lastReview self.pendingReviews.lastReview = now # update last time reviewed if not lastReview: @@ -534,8 +540,9 @@ class Bantracker(callbacks.Plugin): banAge = now - ban.when reviewWindow = lastReview - ban.when - #self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)', channel, ban.mask, - # ban.who, reviewWindow, reviewTime, banAge, reviewTime - reviewWindow) + #self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)', + # channel, ban.mask, ban.who, reviewWindow, reviewTime, + # banAge, reviewTime - reviewWindow) if reviewWindow <= reviewTime < banAge: # ban is old enough, and inside the "review window" try: diff --git a/Bantracker/test.py b/Bantracker/test.py index e9c56c9..399afcc 100644 --- a/Bantracker/test.py +++ b/Bantracker/test.py @@ -20,6 +20,7 @@ import supybot.conf as conf import supybot.ircmsgs as ircmsgs import supybot.world as world +import re import time @@ -241,12 +242,12 @@ class BantrackerTestCase(ChannelPluginTestCase): cb.reviewBans(self.irc) # since it's a forward, it was sent already self.assertFalse(cb.pendingReviews) - self.assertEqual(str(self.irc.takeMsg()).strip(), - "NOTICE #channel :Review: ban 'asd!*@*' set by bot on %s in #test, link: "\ - "%s/bans.cgi?log=1" %(cb.bans['#test'][0].ascwhen, pluginConf.bansite())) - self.assertEqual(str(self.irc.takeMsg()).strip(), - "NOTICE #channel :Review: quiet 'asd!*@*' set by bot on %s in #test, link: "\ - "%s/bans.cgi?log=2" %(cb.bans['#test'][0].ascwhen, pluginConf.bansite())) + self.assertTrue(re.search( + r"^NOTICE #channel :Review: ban 'asd!\*@\*' set by bot on .* in #test,"\ + r" link: .*/bans\.cgi\?log=1$", str(self.irc.takeMsg()).strip())) + self.assertTrue(re.search( + r"^NOTICE #channel :Review: quiet 'asd!\*@\*' set by bot on .* in #test,"\ + r" link: .*/bans\.cgi\?log=2$", str(self.irc.takeMsg()).strip())) def testReviewIgnore(self): pluginConf.review.setValue(True) From 57c349aace784a87212640f5f1d0b3309f9cac85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eli=C3=A1n=20Hanisch?= Date: Mon, 25 Jun 2012 19:40:09 -0300 Subject: [PATCH 03/40] Start hack for the ban autoremoval feature: * BanRemoval class that keeps data about bans that expires. * BanStore class for store BanRemoval objects * testBanAutoRemove testcase right now it doesn't do much. --- Bantracker/plugin.py | 87 +++++++++++++++++++++++++++++++++++++++----- Bantracker/test.py | 11 ++++++ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index 3b80b08..a1d1f6c 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -245,6 +245,60 @@ class ReviewStore(dict): return (host, nick, command, channel, text) +class BanRemoval(object): + """This object saves information about a ban that should be removed when expires""" + def __init__(self, ban, expires): + """ + ban: ban object + expires: time in seconds for it to expire + """ + assert isinstance(ban, Ban), "ban is not a Ban object" + assert isinstance(expires, int), "expire time isn't an integer" + self.ban = ban + self.expires = expires + + def expired(self): + """Check if the ban did expire.""" + if nowSeconds() > (self.ban.when + self.expires): + return True + return False + + +class BanStore(object): + def __init__(self, filename): + # this should be stored into a file + self.shelf = [] + + def __iter__(self): + return iter(self.shelf) + + def __len__(self): + return len(self.shelf) + + def add(self, obj): + self.shelf.append(obj) + + def sort(self): + """Sort bans by expire date""" + def key(x): + return x.ban.when + x.expires + + self.shelf.sort(key=key, reverse=True) + + def popExpired(self): + """Pops a list of expired bans""" + def enumerateReversed(L): + """enumerate in reverse order""" + for i in reversed(xrange(len(L))): + yield i, L[i] + + L = [] + for i, ban in enumerateReversed(self.shelf): + if ban.expired(): + L.append(ban) + del self.shelf[i] + return L + class Bantracker(callbacks.Plugin): """Plugin to manage bans. @@ -274,16 +328,21 @@ class Bantracker(callbacks.Plugin): self.db = None self.get_bans(irc) self.get_nicks(irc) + + # init review stuff self.pendingReviews = ReviewStore('bt.reviews.db') self.pendingReviews.open() self._banreviewfix() - # add scheduled event for check bans that need review, check every hour - try: - schedule.removeEvent(self.name()) - except: - pass - schedule.addPeriodicEvent(lambda : self.reviewBans(irc), 60*60, - name=self.name()) + + # init autoremove stuff + self.managedBans = BanStore('FIXME') + + # add our scheduled events for check bans for reviews or removal + schedule.addPeriodicEvent(lambda: self.reviewBans(irc), 60*60, + name=self.name() + '_review') + schedule.addPeriodicEvent(lambda: self.autoRemoveBans(irc), 60, + name=self.name() + '_autoremove') + def get_nicks(self, irc): self.hosts.clear() @@ -410,7 +469,8 @@ class Bantracker(callbacks.Plugin): except: pass queue.clear() - schedule.removeEvent(self.name()) + schedule.removeEvent(self.name() + '_review') + schedule.removeEvent(self.name() + '_autoremove') self.pendingReviews.close() def reset(self): @@ -638,6 +698,10 @@ class Bantracker(callbacks.Plugin): if not L: del self.pendingReviews[None] + def autoRemoveBans(self, irc=None): + for ban in self.managedBans.popExpired(): + self.log.info('Ban %s expired' % ban.ban.mask) + def doLog(self, irc, channel, s): if not self.registryValue('enabled', channel): return @@ -788,7 +852,12 @@ class Bantracker(callbacks.Plugin): if param[0] in ('+b', '+q'): comment = self.getHostFromBan(irc, msg, mask) - self.doKickban(irc, channel, msg.prefix, mask + realname, extra_comment=comment) + ban = self.doKickban(irc, channel, msg.prefix, mask + realname, + extra_comment=comment) + if True: + # FIXME ban autoremove should be set with a command, but I'm + # lazy. + self.managedBans.add(BanRemoval(ban, 1)) elif param[0] in ('-b', '-q'): self.doUnban(irc,channel, msg.nick, mask + realname) diff --git a/Bantracker/test.py b/Bantracker/test.py index 399afcc..8fb7040 100644 --- a/Bantracker/test.py +++ b/Bantracker/test.py @@ -347,5 +347,16 @@ class BantrackerTestCase(ChannelPluginTestCase): fetch = self.query("SELECT id,channel,mask,operator FROM bans") self.assertEqual((1, '#test', 'troll', 'op'), fetch[0]) + def testBanAutoRemove(self): + cb = self.getCallback() + self.feedBan('asd!*@*') + self.assertTrue(cb.managedBans) # ban in list + cb.autoRemoveBans() + self.assertTrue(cb.managedBans) # ban in list + time.sleep(2) + cb.autoRemoveBans() + self.assertFalse(cb.managedBans) # ban removed + + From 912cc8490c5ecc12aec02f1a4e92acdf4bac7588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eli=C3=A1n=20Hanisch?= Date: Wed, 27 Jun 2012 01:14:51 -0300 Subject: [PATCH 04/40] send -b/-q messages when ban/quiet expires --- Bantracker/plugin.py | 13 +++++++++++-- Bantracker/test.py | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index a1d1f6c..3810ccc 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -142,10 +142,12 @@ class Ban(object): self.id = None if args: # in most ircd: args = (nick, channel, mask, who, when) + self.channel = args[1] self.mask = args[2] self.who = args[3] self.when = float(args[4]) else: + self.channel = kwargs['channel'] self.mask = kwargs['mask'] self.who = kwargs['who'] self.when = float(kwargs['when']) @@ -699,8 +701,15 @@ class Bantracker(callbacks.Plugin): del self.pendingReviews[None] def autoRemoveBans(self, irc=None): + modedict = { 'quiet': '-q', 'ban': '-b' } for ban in self.managedBans.popExpired(): - self.log.info('Ban %s expired' % ban.ban.mask) + channel, mask, type = ban.ban.channel, ban.ban.mask, ban.ban.type + self.log.info("%s '%s' in %s expired", type, + mask, + channel) + # send unban msg + unban = ircmsgs.mode(channel, (modedict[type], mask)) + irc.queueMsg(unban) def doLog(self, irc, channel, s): if not self.registryValue('enabled', channel): @@ -735,7 +744,7 @@ class Bantracker(callbacks.Plugin): self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n)) if extra_comment: self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, extra_comment, n)) - ban = Ban(mask=target, who=operator, when=time.mktime(time.gmtime()), id=id) + ban = Ban(mask=target, who=operator, when=time.mktime(time.gmtime()), id=id, channel=channel) if add_to_cache: if channel not in self.bans: self.bans[channel] = [] diff --git a/Bantracker/test.py b/Bantracker/test.py index 8fb7040..58a7a83 100644 --- a/Bantracker/test.py +++ b/Bantracker/test.py @@ -351,11 +351,14 @@ class BantrackerTestCase(ChannelPluginTestCase): cb = self.getCallback() self.feedBan('asd!*@*') self.assertTrue(cb.managedBans) # ban in list - cb.autoRemoveBans() + cb.autoRemoveBans(self.irc) self.assertTrue(cb.managedBans) # ban in list + print 'waiting 2 secs ...' time.sleep(2) - cb.autoRemoveBans() + cb.autoRemoveBans(self.irc) self.assertFalse(cb.managedBans) # ban removed + msg = self.irc.takeMsg() # unban msg + self.assertEqual(str(msg).strip(), "MODE #test -b :asd!*@*") From 3e584e68807f3affeda8d396c729d4be47e563a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eli=C3=A1n=20Hanisch?= Date: Wed, 4 Jul 2012 00:22:53 -0300 Subject: [PATCH 05/40] add 'banremove' command for set expire time of bans. --- Bantracker/plugin.py | 26 +++++++++++++++++++++++++- Bantracker/test.py | 15 +++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py index 3810ccc..62aeb5b 100644 --- a/Bantracker/plugin.py +++ b/Bantracker/plugin.py @@ -863,7 +863,7 @@ class Bantracker(callbacks.Plugin): comment = self.getHostFromBan(irc, msg, mask) ban = self.doKickban(irc, channel, msg.prefix, mask + realname, extra_comment=comment) - if True: + if False: # FIXME ban autoremove should be set with a command, but I'm # lazy. self.managedBans.add(BanRemoval(ban, 1)) @@ -1284,6 +1284,30 @@ class Bantracker(callbacks.Plugin): irc.error("No comments recorded for ban %i" % id) comment = wrap(comment, ['id', optional('text')]) + def banremove(self, irc, msg, args, id, timespec): + """