ban automatic removal.

This commit is contained in:
Elián Hanisch 2012-09-07 13:52:32 -03:00
commit b69bbb439f
3 changed files with 899 additions and 73 deletions

View File

@ -225,4 +225,15 @@ conf.registerChannelValue(Bantracker.review.forward, 'channels',
registry.SpaceSeparatedListOfStrings([],
"List of channels/nicks to forward the request if the op is in the forward list."))
conf.registerChannelValue(Bantracker, 'autoremove',
registry.Boolean(True,
"""Enable/disable autoremoval of bans."""))
conf.registerChannelValue(Bantracker.autoremove, 'notify',
registry.Boolean(True,
"""Enable/disable notifications of removal of bans."""))
conf.registerChannelValue(Bantracker.autoremove.notify, 'channels',
registry.SpaceSeparatedListOfStrings([],
"""List of channels/nicks to notify about automatic removal of bans."""))

View File

@ -53,11 +53,15 @@ import supybot.ircmsgs as ircmsgs
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.schedule as schedule
import supybot.utils as utils
from supybot.utils.str import format as Format
from fnmatch import fnmatch
from collections import defaultdict
import sqlite
import pytz
import cPickle
import datetime
import csv
import time
import random
import hashlib
@ -70,9 +74,141 @@ 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")}))
class FuzzyDict(dict):
def __getitem__(self, k):
try:
return dict.__getitem__(self, k)
except KeyError:
# ok, lets find the closest match
n = len(k)
keys = [ s for s in self if s[:n] == k ]
if len(keys) != 1:
# ambiguous
raise
return dict.__getitem__(self, keys[0])
timeUnits = FuzzyDict({
'minutes': 60, 'm': 60,
'hours' : 3600, 'M': 2592000,
'days' : 86400,
'weeks' : 604800,
'months' : 2592000,
'years' : 31536000,
})
def readTimeDelta(s):
"""convert a string like "2 days" or "1h2d3w" into seconds"""
# split number and words
if not s:
raise ValueError(s)
digit = string = number = None
seconds = 0
for c in s:
if c == ' ':
continue
if c in '+-0123456789':
if string is None:
# start
digit, string = True, ''
elif digit is False:
digit = True
# completed an unit, add to seconds
string = string.strip()
if string:
try:
unit = timeUnits[string]
except KeyError:
raise ValueError(string)
seconds += number * unit
string = ''
string += c
else:
if digit is None:
# need a number first
raise ValueError(s)
if digit is True:
digit = False
# completed a number
number, string = int(string), ''
string += c
# check last string
if string is None:
raise ValueError(s)
try:
seconds += int(string)
except ValueError:
string = string.strip()
if string:
try:
unit = timeUnits[string]
except KeyError:
raise ValueError(string)
seconds += number * unit
return seconds
# utils.gen.timeElapsed is too noisy, what do I care of the seconds and minutes
# if the period is like a month long, or the zero values?
def timeElapsed(elapsed, short=False, resolution=2):
"""Given <elapsed> seconds, returns a string with an English description of
the amount of time passed.
"""
ret = []
before = False
def Format(s, i):
if i:
if short:
ret.append('%s%s' % (i, s[0]))
else:
ret.append(utils.str.format('%n', (i, s)))
elapsed = int(elapsed)
# Handle negative times
if elapsed < 0:
before = True
elapsed = -elapsed
for s, i in (('year', 31536000), ('month', 2592000), ('week', 604800),
('day', 86400), ('hour', 3600), ('minute', 60)):
count, elapsed = elapsed // i, elapsed % i
Format(s, count)
if len(ret) == resolution:
break
#Format('second', elapsed) # seconds are pointless for now
if not ret:
raise ValueError, 'Time difference not great enough to be noted.'
result = ''
#ret = ret[:resolution]
if short:
result = ' '.join(ret)
else:
result = utils.str.format('%L', ret)
if before:
result += ' ago'
return result
def splitID(s):
"""get a list of integers from a comma separated list of numbers"""
for id in s.split(','):
if id.isdigit():
id = int(id)
if id > 0:
yield id
def capab(user, capability):
capability = capability.lower()
capabilities = list(user.capabilities)
@ -130,16 +266,19 @@ class MsgQueue(object):
queue = MsgQueue()
class Ban(object):
"""Hold my bans"""
def __init__(self, args=None, **kwargs):
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'])
@ -171,17 +310,53 @@ class Ban(object):
def time(self):
return datetime.datetime.fromtimestamp(self.when)
@property
def type(self):
return guessBanType(self.mask)
def serialize(self):
id = self.id
if id is None:
id = ''
return (id, self.channel, self.mask, self.who, self.when)
def deserialize(self, L):
id = L[0]
if id == '':
id = None
else:
id = int(id)
self.id = id
self.channel, self.mask, self.who = L[1:4]
self.when = float(L[4])
self.ascwhen = time.asctime(time.gmtime(self.when))
def guessBanType(mask):
if mask[0] == '%':
return 'quiet'
elif ircutils.isUserHostmask(mask) or mask.endswith('(realname)'):
elif ircutils.isUserHostmask(mask) \
or mask[0] == '$' \
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 +364,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 +380,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))
@ -225,13 +400,116 @@ class PersistentCache(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
"""
self.ban = ban
self.expires = expires
self.notified = False
def __getattr__(self, attr):
return getattr(self.ban, attr)
def timeLeft(self):
return (self.when + self.expires) - nowSeconds()
def expired(self, offset=0):
"""Check if the ban did expire."""
if (nowSeconds() + offset) > (self.when + self.expires):
return True
return False
def serialize(self):
notified = self.notified and 1 or 0
L = [ self.expires, notified ]
L.extend(self.ban.serialize())
return tuple(L)
def deserialize(self, L):
self.expires = int(L[0])
self.notified = bool(int(L[1]))
self.ban = Ban(args=(None, None, None, None, 0))
self.ban.deserialize(L[2:])
def enumerateReversed(L):
"""enumerate in reverse order"""
for i in reversed(xrange(len(L))):
yield i, L[i]
class BanStore(object):
def __init__(self, filename):
self.filename = conf.supybot.directories.data.dirize(filename)
self.shelf = []
def __iter__(self):
return iter(self.shelf)
def __len__(self):
return len(self.shelf)
def open(self):
try:
reader = csv.reader(open(self.filename, 'rb'))
except IOError:
return
for row in reader:
ban = BanRemoval(None, None)
ban.deserialize(row)
self.add(ban)
def close(self):
try:
writer = csv.writer(open(self.filename, 'wb'))
except IOError:
return
for ban in self:
writer.writerow(ban.serialize())
def add(self, obj):
self.shelf.append(obj)
def sort(self):
"""Sort bans by expire date"""
def key(x):
return x.when + x.expires
self.shelf.sort(key=key, reverse=True)
def popExpired(self, time=0):
"""Pops a list of expired bans"""
L = []
for i, ban in enumerateReversed(self.shelf):
if ban.expired(offset=time):
L.append(ban)
del self.shelf[i]
return L
def getExpired(self, time=0):
def generator():
for ban in self.shelf:
if ban.expired(offset=time):
yield ban
return generator()
# opStatus stores in which channels are we currently opped. We define it here
# in a try-except block so it survives if the plugin is reloaded.
try:
opStatus
except:
opStatus = defaultdict(lambda: False)
class Bantracker(callbacks.Plugin):
"""Plugin to manage bans.
See '@list Bantracker' and '@help <command>' for commands"""
noIgnore = True
threaded = True
def __init__(self, irc):
self.__parent = super(Bantracker, self)
self.__parent.__init__(irc)
@ -243,6 +521,8 @@ class Bantracker(callbacks.Plugin):
self.nicks = {}
self.hosts = {}
self.bans = ircutils.IrcDict()
self.opped = opStatus
self.pendingBanremoval = {}
self.thread_timer = threading.Timer(10.0, dequeue, args=(self,irc))
self.thread_timer.start()
@ -254,16 +534,21 @@ class Bantracker(callbacks.Plugin):
self.db = None
self.get_bans(irc)
self.get_nicks(irc)
self.pendingReviews = PersistentCache('bt.reviews.db')
# 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('bt.autoremove.db')
self.managedBans.open()
# add our scheduled events for check bans for reviews or removal
schedule.addPeriodicEvent(lambda: self.reviewBans(irc), 60*60,
'Bantracker_review')
schedule.addPeriodicEvent(lambda: self.autoRemoveBans(irc), 600,
'Bantracker_autoremove')
def get_nicks(self, irc):
self.hosts.clear()
@ -390,8 +675,10 @@ 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()
self.managedBans.close()
def reset(self):
global queue
@ -464,7 +751,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 +766,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 <comment>" %(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 <comment>" \
%(type, mask, channel, prefix, ban.id)
irc.reply(s, to=nick, private=True)
@ -493,12 +781,13 @@ class Bantracker(callbacks.Plugin):
if not reviewTime:
# 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:
now = nowSeconds()
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,17 +799,17 @@ 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
#self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)', channel, ban.mask,
# ban.who, reviewWindow, reviewTime, banAge, reviewTime - reviewWindow)
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:
# ban is old enough, and inside the "review window"
try:
@ -563,11 +852,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 +882,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)
@ -622,6 +905,81 @@ class Bantracker(callbacks.Plugin):
if not L:
del self.pendingReviews[None]
def getOp(self, irc, channel):
msg = ircmsgs.privmsg('Chanserv', "op %s %s" % (channel, irc.nick))
irc.queueMsg(msg)
schedule.addEvent(lambda: self._getOpFail(irc, channel), time.time() + 60,
'Bantracker_getop_%s' % channel)
def _getOpFail(self, irc, channel):
for c in self.registryValue('autoremove.notify.channels', channel):
notice = ircmsgs.notice(c, "Failed to get op in %s" % channel)
irc.queueMsg(notice)
def _getOpOK(self, channel):
try:
schedule.removeEvent('Bantracker_getop_%s' % channel)
return True
except KeyError:
return False
def removeBans(self, irc, channel, modes, deop=False):
# send unban messages, with 4 modes max each.
maxModes = 4
if deop:
modes.append(('-o', irc.nick))
for i in range(len(modes) / maxModes + 1):
L = modes[i * maxModes : (i + 1) * maxModes]
if L:
msg = ircmsgs.mode(channel, ircutils.joinModes(L))
irc.queueMsg(msg)
def autoRemoveBans(self, irc):
modedict = { 'quiet': '-q', 'ban': '-b' }
unbandict = defaultdict(list)
for ban in self.managedBans.popExpired():
channel, mask, type = ban.channel, ban.mask, ban.type
if not self.registryValue('autoremove', channel):
continue
if type == 'quiet':
mask = mask[1:]
self.log.info("%s [%s] %s in %s expired", type,
ban.id,
mask,
channel)
unbandict[channel].append((modedict[type], mask))
for channel, modes in unbandict.iteritems():
if not self.opped[channel]:
self.pendingBanremoval[channel] = modes
self.getOp(irc, channel)
else:
self.removeBans(irc, channel, modes)
# notify about bans soon to expire
for ban in self.managedBans.getExpired(600):
if ban.notified:
continue
channel = ban.channel
if not self.registryValue('autoremove', channel) \
or not self.registryValue('autoremove.notify', channel):
continue
type, mask = ban.type, ban.mask
if type == 'quiet':
mask = mask[1:]
for c in self.registryValue('autoremove.notify.channels', channel):
notice = ircmsgs.notice(c, "%s %s%s%s %s in %s will expire in a few minutes." \
% (type,
ircutils.mircColor('[', 'light green'),
ircutils.bold(ban.id),
ircutils.mircColor(']', 'light green'),
ircutils.mircColor(mask, 'teal'),
ircutils.mircColor(channel, 'teal')))
irc.queueMsg(notice)
ban.notified = True
def doLog(self, irc, channel, s):
if not self.registryValue('enabled', channel):
return
@ -655,7 +1013,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] = []
@ -670,11 +1028,13 @@ class Bantracker(callbacks.Plugin):
self.db_run("UPDATE bans SET removal=%s , removal_op=%s WHERE id=%s", (now(), nick, int(data[0][0])))
if not channel in self.bans:
self.bans[channel] = []
for ban in self.bans[channel]:
for idx, ban in enumerateReversed(self.bans[channel]):
if ban.mask == mask:
idx = self.bans[channel].index(ban)
del self.bans[channel][idx]
# we don't break here because bans might be duplicated.
for idx, br in enumerateReversed(self.managedBans.shelf):
if (channel == br.ban.channel) and (mask == br.ban.mask):
del self.managedBans.shelf[idx]
def doPrivmsg(self, irc, msg):
(recipients, text) = msg.args
@ -756,24 +1116,38 @@ class Bantracker(callbacks.Plugin):
' '.join(msg.args[2:])))
modes = ircutils.separateModes(msg.args[1:])
for param in modes:
realname = ''
mode = param[0]
mask = ''
comment=None
if param[0] not in ("+b", "-b", "+q", "-q"):
# op stuff
if mode[1] == "o":
if ircutils.nickEqual(irc.nick, param[1]):
opped = self.opped[channel] = mode[0] == '+'
if opped == True:
opped_ok = self._getOpOK(channel)
# check if we have bans to remove
if channel in self.pendingBanremoval:
modes = self.pendingBanremoval.pop(channel)
self.removeBans(irc, channel, modes, deop=opped_ok)
continue
# channel mask stuff
realname = mask = ''
comment = None
if mode[1] not in "bq":
continue
mask = param[1]
if mask.startswith("$r:"):
mask = mask[3:]
realname = ' (realname)'
if param[0][1] == 'q':
if mode[1] == 'q':
mask = '%' + mask
if param[0] in ('+b', '+q'):
if mode[0] == '+':
comment = self.getHostFromBan(irc, msg, mask)
self.doKickban(irc, channel, msg.prefix, mask + realname, extra_comment=comment)
elif param[0] in ('-b', '-q'):
ban = self.doKickban(irc, channel, msg.prefix, mask + realname,
extra_comment=comment)
elif mode[0] == '-':
self.doUnban(irc,channel, msg.nick, mask + realname)
def getHostFromBan(self, irc, msg, mask):
@ -1165,30 +1539,202 @@ class Bantracker(callbacks.Plugin):
updatebt = wrap(updatebt, [optional('anything', default=None)])
def comment(self, irc, msg, args, id, kickmsg):
"""<id> [<comment>]
def _getBan(self, id):
"""gets mask, channel and removal date of ban"""
L = self.db_run("SELECT mask, channel, removal FROM bans WHERE id = %s",
id, expect_result=True)
if not L:
raise ValueError
return L[0]
Reads or adds the <comment> for the ban with <id>,
use @bansearch to find the id of a ban
def _setBanDuration(self, id, duration):
"""Set ban for remove after <duration> time, if <duration> is negative
or zero, never remove the ban.
"""
# check if ban has already a duration time
for idx, br in enumerate(self.managedBans):
if id == br.id:
ban = br.ban
del self.managedBans.shelf[idx]
break
else:
if duration < 1:
# nothing to do.
raise Exception("ban isn't marked for removal")
# ban obj ins't in self.managedBans
try:
mask, channel, removal = self._getBan(id)
except ValueError:
raise Exception("unknow id")
type = guessBanType(mask)
if type not in ('ban', 'quiet'):
raise Exception("not a ban or quiet")
if removal:
raise Exception("ban was removed")
for ban in self.bans[channel]:
if mask == ban.mask:
if ban.id is None:
ban.id = id
break
else:
# ban not in sync it seems, shouldn't happen normally.
raise Exception("bans not in sync")
# add ban duration if is positive and non-zero
if duration > 0:
self.managedBans.add(BanRemoval(ban, duration))
def comment(self, irc, msg, args, ids, kickmsg):
"""<id>[,<id> ...] [<comment>][, <duration>]
Reads or adds the <comment> for the ban with <id>, use @bansearch to
find the id of a ban. Using <duration> will set the duration of the ban.
"""
def addComment(id, nick, msg):
n = now()
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, msg, n))
def readComment(id):
return self.db_run("SELECT who, comment, time FROM comments WHERE ban_id=%i", (id,), True)
nick = msg.nick
if kickmsg:
addComment(id, nick, kickmsg)
irc.replySuccess()
else:
data = readComment(id)
if data:
for c in data:
irc.reply("%s %s: %s" % (cPickle.loads(c[2]).astimezone(pytz.timezone('UTC')).strftime("%b %d %Y %H:%M:%S"), c[0], c[1].strip()) )
duration, banset = None, []
if kickmsg and ',' in kickmsg:
s = kickmsg[kickmsg.rfind(',') + 1:]
try:
duration = readTimeDelta(s)
except ValueError:
pass
for id in splitID(ids):
try:
self._getBan(id)
except ValueError:
irc.reply("I don't know any ban with id %s." % id)
continue
if kickmsg:
addComment(id, nick, kickmsg)
if duration is not None:
# set duration time
try:
self._setBanDuration(id, duration)
banset.append(str(id))
except Exception as exc:
irc.reply("Failed to set duration time on %s (%s)" % (id, exc))
else:
irc.error("No comments recorded for ban %i" % id)
comment = wrap(comment, ['id', optional('text')])
data = readComment(id)
if data:
for c in data:
date = cPickle.loads(c[2]).astimezone(pytz.timezone('UTC')).strftime("%b %d %Y %H:%M")
irc.reply("%s %s: %s" % (date, c[0], c[1].strip()))
else:
irc.reply("No comments recorded for ban %s" % id)
# success reply. If duration time used, say which ones were set.
if kickmsg:
if banset:
if duration < 1:
irc.reply(Format("Comment added. %L won't expire.", banset))
return
try:
time = 'after ' + timeElapsed(duration)
except ValueError:
time = 'soon'
irc.reply(Format("Comment added. %L will be removed %s.",
banset, time))
else:
# only a comment
irc.reply("Comment added.")
comment = wrap(comment, ['something', optional('text')])
def duration(self, irc, msg, args, ids, duration):
"""[<id>[,<id> ...]] [<duration>]
Sets the duration of a ban. If <duration> isn't given show when a ban expires. f no <id> is given shows the ids of bans set to expire.
"""
if ids is None:
count = len(self.managedBans)
L = [ str(item.id) for item in self.managedBans ]
irc.reply(Format("%n set to expire: %L", (count, 'ban'), L))
return
if duration is not None:
try:
duration = readTimeDelta(duration)
except ValueError:
irc.error("bad time format.")
return
banset = []
for id in splitID(ids):
if duration is not None:
# set ban duration
try:
self._setBanDuration(id, duration)
banset.append(str(id))
except Exception as exc:
irc.reply("Failed to set duration time on %s (%s)" \
% (id, exc))
else:
# get ban information
try:
mask, channel, removal = self._getBan(id)
except ValueError:
irc.reply("I don't know any ban with id %s." % id)
continue
type = guessBanType(mask)
if type == 'quiet':
mask = mask[1:]
for br in self.managedBans:
if br.id == id:
break
else:
br = None
expires = None
if br:
expires = br.timeLeft()
if expires > 0:
try:
expires = "expires in %s" % timeElapsed(expires)
except ValueError:
expires = "expires soon"
else:
expires = "expired and will be removed soon"
else:
if type in ('quiet', 'ban'):
if not removal:
expires = "never expires"
else:
expires = "not active"
if expires:
irc.reply("[%s] %s - %s - %s - %s" % (id, type, mask, channel, expires))
else:
irc.reply("[%s] %s - %s - %s" % (id, type, mask, channel))
# reply with the bans ids that were correctly set.
if banset:
if duration < 1:
irc.reply(Format("%L won't expire.", banset))
return
try:
time = 'after ' + timeElapsed(duration)
except ValueError:
time = 'soon'
irc.reply(Format("%L will be removed %s.", banset, time))
duration = wrap(duration, [optional('something'), optional('text')])
def banlink(self, irc, msg, args, id, highlight):
"""<id> [<highlight>]
@ -1225,9 +1771,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

View File

@ -20,6 +20,7 @@ import supybot.conf as conf
import supybot.ircmsgs as ircmsgs
import supybot.world as world
import re
import time
@ -56,6 +57,7 @@ class BantrackerTestCase(ChannelPluginTestCase):
else:
return f(*args, **kwargs)
cb.check_auth = test_check_auth
cb.opped.clear()
def setDb(self):
import sqlite, os
@ -123,7 +125,65 @@ class BantrackerTestCase(ChannelPluginTestCase):
self.irc.feedMsg(ban)
return ban
def op(self):
msg = ircmsgs.mode(self.channel, ('+o', self.irc.nick),
'Chanserv!service@service')
self.irc.feedMsg(msg)
def deop(self):
msg = ircmsgs.mode(self.channel, ('-o', self.irc.nick),
'Chanserv!service@service')
self.irc.feedMsg(msg)
def testComment(self):
self.assertResponse('comment 1', "I don't know any ban with id 1.")
self.feedBan('asd!*@*')
self.assertResponse('comment 1', 'No comments recorded for ban 1')
self.assertResponse('comment 1 this is a test',
'Comment added.')
self.assertRegexp('comment 1', 'test: this is a test$')
self.assertResponse('comment 1 this is a test, another test',
'Comment added.')
self.feedBan('nick', mode='k')
self.assertResponse('comment 2 this is a kick, 2week',
"Failed to set duration time on 2 (not a ban or quiet)")
msg = self.irc.takeMsg()
self.assertEqual(msg.args[1], 'test: Comment added.')
self.assertResponse('comment 1 not a valid, duration 2',
'Comment added.')
def testMultiComment(self):
self.feedBan('asd!*@*')
self.feedBan('qwe!*@*')
self.assertResponse('comment 1,2,3 this is a test, 2 days',
"I don't know any ban with id 3.")
msg = self.irc.takeMsg()
self.assertEqual(msg.args[1],
"test: Comment added. 1 and 2 will be removed after 2 days.")
self.assertRegexp('comment 1,2', 'test: this is a test, 2 days$')
msg = self.irc.takeMsg()
self.assertTrue(msg.args[1].endswith("test: this is a test, 2 days"))
def testCommentDuration(self):
self.feedBan('asd!*@*')
self.assertResponse('comment 1 this is a test, 1 week 10',
'Comment added. 1 will be removed after 1 week.')
self.assertRegexp('comment 1', 'test: this is a test, 1 week 10$')
self.assertRegexp('duration 1', 'expires in 1 week$')
def testCommentDurationRemove(self):
self.feedBan('asd!*@*')
self.assertResponse('comment 1 this is a test, -10',
"Failed to set duration time on 1 (ban isn't marked for removal)")
msg = self.irc.takeMsg()
self.assertEqual(msg.args[1], 'test: Comment added.')
self.assertResponse('comment 1 this is a test, 10',
'Comment added. 1 will be removed soon.')
self.assertResponse('comment 1 this is a test, -10',
"Comment added. 1 won't expire.")
self.assertRegexp('duration 1', 'never expires$')
def testCommentRequest(self):
pluginConf.request.setValue(True)
# test bans
self.feedBan('asd!*@*')
@ -241,12 +301,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)
@ -286,7 +346,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')
@ -346,5 +406,214 @@ class BantrackerTestCase(ChannelPluginTestCase):
fetch = self.query("SELECT id,channel,mask,operator FROM bans")
self.assertEqual((1, '#test', 'troll', 'op'), fetch[0])
def testDuration(self):
self.op()
cb = self.getCallback()
self.feedBan('asd!*@*')
cb.autoRemoveBans(self.irc)
self.assertFalse(cb.managedBans)
self.assertResponse('duration 1 1', "1 will be removed soon.")
self.assertTrue(cb.managedBans) # ban in list
print 'waiting 2 secs ...'
time.sleep(2)
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!*@*")
def testDurationRemove(self):
self.feedBan('asd!*@*')
self.assertResponse('duration 1 -1',
"Failed to set duration time on 1 (ban isn't marked for removal)")
self.assertResponse('duration 1 10', '1 will be removed soon.')
self.assertResponse('duration 1 -1', "1 won't expire.")
self.assertRegexp('duration 1', 'never expires')
def testDurationMergeModes(self):
self.op()
cb = self.getCallback()
self.feedBan('asd!*@*')
self.feedBan('qwe!*@*')
self.feedBan('zxc!*@*')
self.feedBan('asd!*@*', mode='q')
self.feedBan('qwe!*@*', mode='q')
self.feedBan('zxc!*@*', mode='q')
self.assertNotError('duration 1,2,3,4,5,6 1')
print 'waiting 2 secs ...'
time.sleep(2)
cb.autoRemoveBans(self.irc)
msg = self.irc.takeMsg() # unban msg
self.assertEqual(str(msg).strip(),
"MODE #test -qqqb zxc!*@* qwe!*@* asd!*@* :zxc!*@*")
msg = self.irc.takeMsg()
self.assertEqual(str(msg).strip(), "MODE #test -bb qwe!*@* :asd!*@*")
def testDurationMultiSet(self):
self.feedBan('asd!*@*')
self.assertResponse('duration 1,2 10d',
"Failed to set duration time on 2 (unknow id)")
msg = self.irc.takeMsg()
self.assertEqual(msg.args[1],
"test: 1 will be removed after 1 week and 3 days.")
def testDurationQuiet(self):
self.op()
cb = self.getCallback()
self.feedBan('asd!*@*', mode='q')
self.assertNotError('duration 1 1')
print 'waiting 2 sec ...'
time.sleep(2)
cb.autoRemoveBans(self.irc)
msg = self.irc.takeMsg() # unban msg
self.assertEqual(str(msg).strip(), "MODE #test -q :asd!*@*")
def testDurationBadType(self):
self.feedBan('nick', mode='k')
self.assertResponse('duration 1 1',
"Failed to set duration time on 1 (not a ban or quiet)")
self.feedBan('$a:nick')
self.assertResponse('duration 2 1', '2 will be removed soon.')
def testDurationBadId(self):
self.assertResponse('duration 1 1',
"Failed to set duration time on 1 (unknow id)")
def testDurationInactiveBan(self):
self.feedBan('asd!*@*')
self.irc.feedMsg(ircmsgs.unban(self.channel, 'asd!*@*',
'op!user@host.net'))
self.assertResponse('duration 1 1',
"Failed to set duration time on 1 (ban was removed)")
def testDurationTimeFormat(self):
cb = self.getCallback()
self.feedBan('asd!*@*')
self.assertNotError('duration 1 10m')
self.assertEqual(cb.managedBans.shelf[0].expires, 600)
self.assertNotError('duration 1 2 weeks')
self.assertEqual(cb.managedBans.shelf[0].expires, 1209600)
self.assertNotError('duration 1 1m 2 days')
self.assertEqual(cb.managedBans.shelf[0].expires, 172860)
self.assertNotError('duration 1 24h 1day')
self.assertEqual(cb.managedBans.shelf[0].expires, 172800)
self.assertNotError('duration 1 1m1h1d1w1M1y')
self.assertEqual(cb.managedBans.shelf[0].expires, 34822860)
self.assertNotError('duration 1 999')
self.assertEqual(cb.managedBans.shelf[0].expires, 999)
def testDurationTimeFormatBad(self):
self.assertError('duration 1 10 apples')
def testDurationNotice(self):
cb = self.getCallback()
self.feedBan('asd!*@*')
self.assertNotError('duration 1 300')
pluginConf.autoremove.notify.channels.set('#test')
try:
cb.autoRemoveBans(self.irc)
msg = self.irc.takeMsg()
self.assertEqual(str(msg).strip(),
"NOTICE #test :ban \x0309[\x03\x021\x02\x0309]\x03 \x0310asd!*@*\x03"\
" in \x0310#test\x03 will expire in a few minutes.")
# don't send the notice again.
cb.autoRemoveBans(self.irc)
self.assertFalse(self.irc.takeMsg())
finally:
pluginConf.autoremove.notify.channels.set('')
def testAutoremoveStore(self):
self.feedBan('asd!*@*')
self.feedBan('qwe!*@*')
self.feedBan('zxc!*@*', mode='q')
self.assertNotError('duration 1 10m')
self.assertNotError('duration 2 1d')
self.assertNotError('duration 3 1w')
cb = self.getCallback()
cb.managedBans.shelf[1].notified = True
cb.managedBans.close()
cb.managedBans.shelf = []
cb.managedBans.open()
L = cb.managedBans.shelf
for i, n in enumerate((600, 86400, 604800)):
self.assertEqual(L[i].expires, n)
for i, n in enumerate((False, True, False)):
self.assertEqual(L[i].notified, n)
for i, n in enumerate((1, 2, 3)):
self.assertEqual(L[i].ban.id, n)
for i, n in enumerate(('asd!*@*', 'qwe!*@*', '%zxc!*@*')):
self.assertEqual(L[i].ban.mask, n)
self.assertEqual(L[0].ban.channel, '#test')
def testBaninfo(self):
cb = self.getCallback()
self.feedBan('asd!*@*')
self.assertResponse('duration 1',
"[1] ban - asd!*@* - #test - never expires")
self.assertNotError('duration 1 10')
self.assertResponse('duration 1',
"[1] ban - asd!*@* - #test - expires soon")
self.assertNotError('duration 1 34502')
self.assertResponse('duration 1',
"[1] ban - asd!*@* - #test - expires in 9 hours and 35 minutes")
self.irc.feedMsg(ircmsgs.unban(self.channel, 'asd!*@*',
'op!user@host.net'))
self.assertResponse('duration 1',
"[1] ban - asd!*@* - #test - not active")
def testBaninfoGeneral(self):
cb = self.getCallback()
self.feedBan('asd!*@*')
self.feedBan('qwe!*@*')
self.assertNotError('duration 1 1d')
self.assertResponse('duration', "1 ban set to expire: 1")
self.assertNotError('duration 2 1d')
self.assertResponse('duration', "2 bans set to expire: 1 and 2")
def testOpTrack(self):
cb = self.getCallback()
self.assertEqual(cb.opped['#test'], False)
self.op()
self.assertEqual(cb.opped['#test'], True)
self.deop()
self.assertEqual(cb.opped['#test'], False)
def testOpDuration(self):
cb = self.getCallback()
self.feedBan('asd!*@*')
self.assertNotError('duration 1 1')
print 'waiting 2 secs ...'
time.sleep(2)
cb.autoRemoveBans(self.irc)
msg = self.irc.takeMsg() # op msg
self.assertEqual(str(msg).strip(), "PRIVMSG Chanserv :op #test test")
self.op()
msg = self.irc.takeMsg() # unban msg
self.assertEqual(str(msg).strip(), "MODE #test -bo asd!*@* :test")
def testOpFail(self):
import supybot.drivers as drivers
import supybot.schedule as schedule
pluginConf.autoremove.notify.channels.set('#test')
try:
cb = self.getCallback()
self.feedBan('asd!*@*')
self.assertNotError('duration 1 1')
print 'waiting 4 secs ...'
time.sleep(2)
cb.autoRemoveBans(self.irc)
msg = self.irc.takeMsg() # op msg
self.assertEqual(str(msg).strip(), "PRIVMSG Chanserv :op #test test")
schedule.rescheduleEvent('Bantracker_getop_#test', 1)
time.sleep(2)
drivers.run()
msg = self.irc.takeMsg() # fail msg
self.assertEqual(str(msg).strip(),
"NOTICE #test :Failed to get op in #test")
self.op()
msg = self.irc.takeMsg() # unban msg
self.assertEqual(str(msg).strip(), "MODE #test -b :asd!*@*")
finally:
pluginConf.autoremove.notify.channels.set('')