ban automatic removal.
This commit is contained in:
@ -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."""))
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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('')
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user