ban automatic removal.
This commit is contained in:
commit
b69bbb439f
|
@ -225,4 +225,15 @@ conf.registerChannelValue(Bantracker.review.forward, 'channels',
|
||||||
registry.SpaceSeparatedListOfStrings([],
|
registry.SpaceSeparatedListOfStrings([],
|
||||||
"List of channels/nicks to forward the request if the op is in the forward list."))
|
"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.conf as conf
|
||||||
import supybot.ircdb as ircdb
|
import supybot.ircdb as ircdb
|
||||||
import supybot.schedule as schedule
|
import supybot.schedule as schedule
|
||||||
|
import supybot.utils as utils
|
||||||
|
from supybot.utils.str import format as Format
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
from collections import defaultdict
|
||||||
import sqlite
|
import sqlite
|
||||||
import pytz
|
import pytz
|
||||||
import cPickle
|
import cPickle
|
||||||
import datetime
|
import datetime
|
||||||
|
import csv
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -70,9 +74,141 @@ tz = 'UTC'
|
||||||
def now():
|
def now():
|
||||||
return cPickle.dumps(datetime.datetime.now(pytz.timezone(tz)))
|
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):
|
def fromTime(x):
|
||||||
return cPickle.dumps(datetime.datetime(*time.gmtime(x)[:6], **{'tzinfo': pytz.timezone("UTC")}))
|
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):
|
def capab(user, capability):
|
||||||
capability = capability.lower()
|
capability = capability.lower()
|
||||||
capabilities = list(user.capabilities)
|
capabilities = list(user.capabilities)
|
||||||
|
@ -130,16 +266,19 @@ class MsgQueue(object):
|
||||||
|
|
||||||
queue = MsgQueue()
|
queue = MsgQueue()
|
||||||
|
|
||||||
|
|
||||||
class Ban(object):
|
class Ban(object):
|
||||||
"""Hold my bans"""
|
"""Hold my bans"""
|
||||||
def __init__(self, args=None, **kwargs):
|
def __init__(self, args=None, **kwargs):
|
||||||
self.id = None
|
self.id = None
|
||||||
if args:
|
if args:
|
||||||
# in most ircd: args = (nick, channel, mask, who, when)
|
# in most ircd: args = (nick, channel, mask, who, when)
|
||||||
|
self.channel = args[1]
|
||||||
self.mask = args[2]
|
self.mask = args[2]
|
||||||
self.who = args[3]
|
self.who = args[3]
|
||||||
self.when = float(args[4])
|
self.when = float(args[4])
|
||||||
else:
|
else:
|
||||||
|
self.channel = kwargs['channel']
|
||||||
self.mask = kwargs['mask']
|
self.mask = kwargs['mask']
|
||||||
self.who = kwargs['who']
|
self.who = kwargs['who']
|
||||||
self.when = float(kwargs['when'])
|
self.when = float(kwargs['when'])
|
||||||
|
@ -171,17 +310,53 @@ class Ban(object):
|
||||||
def time(self):
|
def time(self):
|
||||||
return datetime.datetime.fromtimestamp(self.when)
|
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):
|
def guessBanType(mask):
|
||||||
if mask[0] == '%':
|
if mask[0] == '%':
|
||||||
return 'quiet'
|
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 'ban'
|
||||||
return 'removal'
|
return 'removal'
|
||||||
|
|
||||||
class PersistentCache(dict):
|
|
||||||
|
class ReviewStore(dict):
|
||||||
def __init__(self, filename):
|
def __init__(self, filename):
|
||||||
self.filename = conf.supybot.directories.data.dirize(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):
|
def open(self):
|
||||||
import csv
|
import csv
|
||||||
|
@ -189,7 +364,7 @@ class PersistentCache(dict):
|
||||||
reader = csv.reader(open(self.filename, 'rb'))
|
reader = csv.reader(open(self.filename, 'rb'))
|
||||||
except IOError:
|
except IOError:
|
||||||
return
|
return
|
||||||
self.time = int(reader.next()[1])
|
self.lastReview = int(reader.next()[1])
|
||||||
for row in reader:
|
for row in reader:
|
||||||
host, value = self.deserialize(*row)
|
host, value = self.deserialize(*row)
|
||||||
try:
|
try:
|
||||||
|
@ -205,7 +380,7 @@ class PersistentCache(dict):
|
||||||
writer = csv.writer(open(self.filename, 'wb'))
|
writer = csv.writer(open(self.filename, 'wb'))
|
||||||
except IOError:
|
except IOError:
|
||||||
return
|
return
|
||||||
writer.writerow(('time', str(int(self.time))))
|
writer.writerow(('time', str(int(self.lastReview))))
|
||||||
for host, values in self.iteritems():
|
for host, values in self.iteritems():
|
||||||
for v in values:
|
for v in values:
|
||||||
writer.writerow(self.serialize(host, v))
|
writer.writerow(self.serialize(host, v))
|
||||||
|
@ -225,13 +400,116 @@ class PersistentCache(dict):
|
||||||
return (host, nick, command, channel, text)
|
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):
|
class Bantracker(callbacks.Plugin):
|
||||||
"""Plugin to manage bans.
|
"""Plugin to manage bans.
|
||||||
See '@list Bantracker' and '@help <command>' for commands"""
|
See '@list Bantracker' and '@help <command>' for commands"""
|
||||||
noIgnore = True
|
noIgnore = True
|
||||||
threaded = True
|
threaded = True
|
||||||
|
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
self.__parent = super(Bantracker, self)
|
self.__parent = super(Bantracker, self)
|
||||||
self.__parent.__init__(irc)
|
self.__parent.__init__(irc)
|
||||||
|
@ -243,6 +521,8 @@ class Bantracker(callbacks.Plugin):
|
||||||
self.nicks = {}
|
self.nicks = {}
|
||||||
self.hosts = {}
|
self.hosts = {}
|
||||||
self.bans = ircutils.IrcDict()
|
self.bans = ircutils.IrcDict()
|
||||||
|
self.opped = opStatus
|
||||||
|
self.pendingBanremoval = {}
|
||||||
|
|
||||||
self.thread_timer = threading.Timer(10.0, dequeue, args=(self,irc))
|
self.thread_timer = threading.Timer(10.0, dequeue, args=(self,irc))
|
||||||
self.thread_timer.start()
|
self.thread_timer.start()
|
||||||
|
@ -254,16 +534,21 @@ class Bantracker(callbacks.Plugin):
|
||||||
self.db = None
|
self.db = None
|
||||||
self.get_bans(irc)
|
self.get_bans(irc)
|
||||||
self.get_nicks(irc)
|
self.get_nicks(irc)
|
||||||
self.pendingReviews = PersistentCache('bt.reviews.db')
|
|
||||||
|
# init review stuff
|
||||||
|
self.pendingReviews = ReviewStore('bt.reviews.db')
|
||||||
self.pendingReviews.open()
|
self.pendingReviews.open()
|
||||||
self._banreviewfix()
|
self._banreviewfix()
|
||||||
# add scheduled event for check bans that need review, check every hour
|
|
||||||
try:
|
# init autoremove stuff
|
||||||
schedule.removeEvent(self.name())
|
self.managedBans = BanStore('bt.autoremove.db')
|
||||||
except:
|
self.managedBans.open()
|
||||||
pass
|
|
||||||
schedule.addPeriodicEvent(lambda : self.reviewBans(irc), 60*60,
|
# add our scheduled events for check bans for reviews or removal
|
||||||
name=self.name())
|
schedule.addPeriodicEvent(lambda: self.reviewBans(irc), 60*60,
|
||||||
|
'Bantracker_review')
|
||||||
|
schedule.addPeriodicEvent(lambda: self.autoRemoveBans(irc), 600,
|
||||||
|
'Bantracker_autoremove')
|
||||||
|
|
||||||
def get_nicks(self, irc):
|
def get_nicks(self, irc):
|
||||||
self.hosts.clear()
|
self.hosts.clear()
|
||||||
|
@ -390,8 +675,10 @@ class Bantracker(callbacks.Plugin):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
queue.clear()
|
queue.clear()
|
||||||
schedule.removeEvent(self.name())
|
schedule.removeEvent(self.name() + '_review')
|
||||||
|
schedule.removeEvent(self.name() + '_autoremove')
|
||||||
self.pendingReviews.close()
|
self.pendingReviews.close()
|
||||||
|
self.managedBans.close()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
global queue
|
global queue
|
||||||
|
@ -464,7 +751,7 @@ class Bantracker(callbacks.Plugin):
|
||||||
return
|
return
|
||||||
# check the type of the action taken
|
# check the type of the action taken
|
||||||
mask = ban.mask
|
mask = ban.mask
|
||||||
type = guessBanType(mask)
|
type = ban.type
|
||||||
if type == 'quiet':
|
if type == 'quiet':
|
||||||
mask = mask[1:]
|
mask = mask[1:]
|
||||||
# check if type is enabled
|
# check if type is enabled
|
||||||
|
@ -479,11 +766,12 @@ class Bantracker(callbacks.Plugin):
|
||||||
if nickMatch(nick, self.registryValue('request.ignore', channel)):
|
if nickMatch(nick, self.registryValue('request.ignore', channel)):
|
||||||
return
|
return
|
||||||
if nickMatch(nick, self.registryValue('request.forward', channel)):
|
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:"\
|
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)
|
" %scomment %s <comment>" %(type, mask, channel, nick, prefix, ban.id)
|
||||||
self._sendForward(irc, s, 'request', channel)
|
self._sendForward(irc, s, 'request', channel)
|
||||||
else:
|
else:
|
||||||
# send to op
|
# send to operator
|
||||||
s = "Please comment on the %s of %s in %s, use: %scomment %s <comment>" \
|
s = "Please comment on the %s of %s in %s, use: %scomment %s <comment>" \
|
||||||
%(type, mask, channel, prefix, ban.id)
|
%(type, mask, channel, prefix, ban.id)
|
||||||
irc.reply(s, to=nick, private=True)
|
irc.reply(s, to=nick, private=True)
|
||||||
|
@ -493,12 +781,13 @@ class Bantracker(callbacks.Plugin):
|
||||||
if not reviewTime:
|
if not reviewTime:
|
||||||
# time is zero, do nothing
|
# time is zero, do nothing
|
||||||
return
|
return
|
||||||
now = time.mktime(time.gmtime())
|
|
||||||
lastreview = self.pendingReviews.time
|
now = nowSeconds()
|
||||||
self.pendingReviews.time = now # update last time reviewed
|
lastReview = self.pendingReviews.lastReview
|
||||||
if not lastreview:
|
self.pendingReviews.lastReview = now # update last time reviewed
|
||||||
|
if not lastReview:
|
||||||
# initialize last time reviewed timestamp
|
# initialize last time reviewed timestamp
|
||||||
lastreview = now - reviewTime
|
lastReview = now - reviewTime
|
||||||
|
|
||||||
for channel, bans in self.bans.iteritems():
|
for channel, bans in self.bans.iteritems():
|
||||||
if not self.registryValue('enabled', channel) \
|
if not self.registryValue('enabled', channel) \
|
||||||
|
@ -510,17 +799,17 @@ class Bantracker(callbacks.Plugin):
|
||||||
# the less I touch it the better.
|
# the less I touch it the better.
|
||||||
if ban.mask.endswith('$#ubuntu-read-topic'):
|
if ban.mask.endswith('$#ubuntu-read-topic'):
|
||||||
continue
|
continue
|
||||||
type = guessBanType(ban.mask)
|
|
||||||
if type == 'removal':
|
type = ban.type
|
||||||
# skip kicks
|
if type in ('removal', 'mark'):
|
||||||
continue
|
# skip kicks and marks
|
||||||
if not ('*' in ban.mask or '?' in ban.mask or '$' in ban.mask):
|
|
||||||
# XXX hack over hack, we are supposing these are marks.
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
banAge = now - ban.when
|
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,
|
#self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)',
|
||||||
# ban.who, reviewWindow, reviewTime, banAge, reviewTime - reviewWindow)
|
# channel, ban.mask, ban.who, reviewWindow, reviewTime,
|
||||||
|
# banAge, reviewTime - reviewWindow)
|
||||||
if reviewWindow <= reviewTime < banAge:
|
if reviewWindow <= reviewTime < banAge:
|
||||||
# ban is old enough, and inside the "review window"
|
# ban is old enough, and inside the "review window"
|
||||||
try:
|
try:
|
||||||
|
@ -563,11 +852,8 @@ class Bantracker(callbacks.Plugin):
|
||||||
self.registryValue('bansite'),
|
self.registryValue('bansite'),
|
||||||
ban.id)
|
ban.id)
|
||||||
msg = ircmsgs.privmsg(nick, s)
|
msg = ircmsgs.privmsg(nick, s)
|
||||||
if host in self.pendingReviews \
|
if (nick, msg) not in self.pendingReviews[host]:
|
||||||
and (nick, msg) not in self.pendingReviews[host]:
|
|
||||||
self.pendingReviews[host].append((nick, msg))
|
self.pendingReviews[host].append((nick, msg))
|
||||||
else:
|
|
||||||
self.pendingReviews[host] = [(nick, msg)]
|
|
||||||
elif banAge < reviewTime:
|
elif banAge < reviewTime:
|
||||||
# since we made sure bans are sorted by time, the bans left are more recent
|
# since we made sure bans are sorted by time, the bans left are more recent
|
||||||
break
|
break
|
||||||
|
@ -596,10 +882,7 @@ class Bantracker(callbacks.Plugin):
|
||||||
self.pendingReviews.clear()
|
self.pendingReviews.clear()
|
||||||
|
|
||||||
for host, nick, msg in nodups:
|
for host, nick, msg in nodups:
|
||||||
if host in self.pendingReviews:
|
self.pendingReviews[host].append((nick, msg))
|
||||||
self.pendingReviews[host].append((nick, msg))
|
|
||||||
else:
|
|
||||||
self.pendingReviews[host] = [(nick, msg)]
|
|
||||||
|
|
||||||
def _sendReviews(self, irc, msg):
|
def _sendReviews(self, irc, msg):
|
||||||
host = ircutils.hostFromHostmask(msg.prefix)
|
host = ircutils.hostFromHostmask(msg.prefix)
|
||||||
|
@ -622,6 +905,81 @@ class Bantracker(callbacks.Plugin):
|
||||||
if not L:
|
if not L:
|
||||||
del self.pendingReviews[None]
|
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):
|
def doLog(self, irc, channel, s):
|
||||||
if not self.registryValue('enabled', channel):
|
if not self.registryValue('enabled', channel):
|
||||||
return
|
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))
|
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n))
|
||||||
if extra_comment:
|
if extra_comment:
|
||||||
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, extra_comment, n))
|
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 add_to_cache:
|
||||||
if channel not in self.bans:
|
if channel not in self.bans:
|
||||||
self.bans[channel] = []
|
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])))
|
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:
|
if not channel in self.bans:
|
||||||
self.bans[channel] = []
|
self.bans[channel] = []
|
||||||
for ban in self.bans[channel]:
|
for idx, ban in enumerateReversed(self.bans[channel]):
|
||||||
if ban.mask == mask:
|
if ban.mask == mask:
|
||||||
idx = self.bans[channel].index(ban)
|
|
||||||
del self.bans[channel][idx]
|
del self.bans[channel][idx]
|
||||||
# we don't break here because bans might be duplicated.
|
# 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):
|
def doPrivmsg(self, irc, msg):
|
||||||
(recipients, text) = msg.args
|
(recipients, text) = msg.args
|
||||||
|
@ -756,24 +1116,38 @@ class Bantracker(callbacks.Plugin):
|
||||||
' '.join(msg.args[2:])))
|
' '.join(msg.args[2:])))
|
||||||
modes = ircutils.separateModes(msg.args[1:])
|
modes = ircutils.separateModes(msg.args[1:])
|
||||||
for param in modes:
|
for param in modes:
|
||||||
realname = ''
|
|
||||||
mode = param[0]
|
mode = param[0]
|
||||||
mask = ''
|
# op stuff
|
||||||
comment=None
|
if mode[1] == "o":
|
||||||
if param[0] not in ("+b", "-b", "+q", "-q"):
|
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
|
continue
|
||||||
|
|
||||||
|
# channel mask stuff
|
||||||
|
realname = mask = ''
|
||||||
|
comment = None
|
||||||
|
if mode[1] not in "bq":
|
||||||
|
continue
|
||||||
|
|
||||||
mask = param[1]
|
mask = param[1]
|
||||||
if mask.startswith("$r:"):
|
if mask.startswith("$r:"):
|
||||||
mask = mask[3:]
|
mask = mask[3:]
|
||||||
realname = ' (realname)'
|
realname = ' (realname)'
|
||||||
|
|
||||||
if param[0][1] == 'q':
|
if mode[1] == 'q':
|
||||||
mask = '%' + mask
|
mask = '%' + mask
|
||||||
|
|
||||||
if param[0] in ('+b', '+q'):
|
if mode[0] == '+':
|
||||||
comment = self.getHostFromBan(irc, msg, mask)
|
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,
|
||||||
elif param[0] in ('-b', '-q'):
|
extra_comment=comment)
|
||||||
|
elif mode[0] == '-':
|
||||||
self.doUnban(irc,channel, msg.nick, mask + realname)
|
self.doUnban(irc,channel, msg.nick, mask + realname)
|
||||||
|
|
||||||
def getHostFromBan(self, irc, msg, mask):
|
def getHostFromBan(self, irc, msg, mask):
|
||||||
|
@ -1165,30 +1539,202 @@ class Bantracker(callbacks.Plugin):
|
||||||
|
|
||||||
updatebt = wrap(updatebt, [optional('anything', default=None)])
|
updatebt = wrap(updatebt, [optional('anything', default=None)])
|
||||||
|
|
||||||
def comment(self, irc, msg, args, id, kickmsg):
|
def _getBan(self, id):
|
||||||
"""<id> [<comment>]
|
"""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>,
|
def _setBanDuration(self, id, duration):
|
||||||
use @bansearch to find the id of a ban
|
"""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):
|
def addComment(id, nick, msg):
|
||||||
n = now()
|
n = now()
|
||||||
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, msg, n))
|
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, msg, n))
|
||||||
|
|
||||||
def readComment(id):
|
def readComment(id):
|
||||||
return self.db_run("SELECT who, comment, time FROM comments WHERE ban_id=%i", (id,), True)
|
return self.db_run("SELECT who, comment, time FROM comments WHERE ban_id=%i", (id,), True)
|
||||||
|
|
||||||
nick = msg.nick
|
nick = msg.nick
|
||||||
if kickmsg:
|
duration, banset = None, []
|
||||||
addComment(id, nick, kickmsg)
|
if kickmsg and ',' in kickmsg:
|
||||||
irc.replySuccess()
|
s = kickmsg[kickmsg.rfind(',') + 1:]
|
||||||
else:
|
try:
|
||||||
data = readComment(id)
|
duration = readTimeDelta(s)
|
||||||
if data:
|
except ValueError:
|
||||||
for c in data:
|
pass
|
||||||
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()) )
|
|
||||||
|
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:
|
else:
|
||||||
irc.error("No comments recorded for ban %i" % id)
|
data = readComment(id)
|
||||||
comment = wrap(comment, ['id', optional('text')])
|
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):
|
def banlink(self, irc, msg, args, id, highlight):
|
||||||
"""<id> [<highlight>]
|
"""<id> [<highlight>]
|
||||||
|
@ -1225,9 +1771,9 @@ class Bantracker(callbacks.Plugin):
|
||||||
nick, host = key.split('@', 1)
|
nick, host = key.split('@', 1)
|
||||||
else:
|
else:
|
||||||
nick, host = key, None
|
nick, host = key, None
|
||||||
try:
|
if host in self.pendingReviews:
|
||||||
reviews = self.pendingReviews[host]
|
reviews = self.pendingReviews[host]
|
||||||
except KeyError:
|
else:
|
||||||
irc.reply('No reviews for %s, use --verbose for check the correct nick@host key.' % key)
|
irc.reply('No reviews for %s, use --verbose for check the correct nick@host key.' % key)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import supybot.conf as conf
|
||||||
import supybot.ircmsgs as ircmsgs
|
import supybot.ircmsgs as ircmsgs
|
||||||
import supybot.world as world
|
import supybot.world as world
|
||||||
|
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ class BantrackerTestCase(ChannelPluginTestCase):
|
||||||
else:
|
else:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
cb.check_auth = test_check_auth
|
cb.check_auth = test_check_auth
|
||||||
|
cb.opped.clear()
|
||||||
|
|
||||||
def setDb(self):
|
def setDb(self):
|
||||||
import sqlite, os
|
import sqlite, os
|
||||||
|
@ -123,7 +125,65 @@ class BantrackerTestCase(ChannelPluginTestCase):
|
||||||
self.irc.feedMsg(ban)
|
self.irc.feedMsg(ban)
|
||||||
return 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):
|
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)
|
pluginConf.request.setValue(True)
|
||||||
# test bans
|
# test bans
|
||||||
self.feedBan('asd!*@*')
|
self.feedBan('asd!*@*')
|
||||||
|
@ -241,12 +301,12 @@ class BantrackerTestCase(ChannelPluginTestCase):
|
||||||
cb.reviewBans(self.irc)
|
cb.reviewBans(self.irc)
|
||||||
# since it's a forward, it was sent already
|
# since it's a forward, it was sent already
|
||||||
self.assertFalse(cb.pendingReviews)
|
self.assertFalse(cb.pendingReviews)
|
||||||
self.assertEqual(str(self.irc.takeMsg()).strip(),
|
self.assertTrue(re.search(
|
||||||
"NOTICE #channel :Review: ban 'asd!*@*' set by bot on %s in #test, link: "\
|
r"^NOTICE #channel :Review: ban 'asd!\*@\*' set by bot on .* in #test,"\
|
||||||
"%s/bans.cgi?log=1" %(cb.bans['#test'][0].ascwhen, pluginConf.bansite()))
|
r" link: .*/bans\.cgi\?log=1$", str(self.irc.takeMsg()).strip()))
|
||||||
self.assertEqual(str(self.irc.takeMsg()).strip(),
|
self.assertTrue(re.search(
|
||||||
"NOTICE #channel :Review: quiet 'asd!*@*' set by bot on %s in #test, link: "\
|
r"^NOTICE #channel :Review: quiet 'asd!\*@\*' set by bot on .* in #test,"\
|
||||||
"%s/bans.cgi?log=2" %(cb.bans['#test'][0].ascwhen, pluginConf.bansite()))
|
r" link: .*/bans\.cgi\?log=2$", str(self.irc.takeMsg()).strip()))
|
||||||
|
|
||||||
def testReviewIgnore(self):
|
def testReviewIgnore(self):
|
||||||
pluginConf.review.setValue(True)
|
pluginConf.review.setValue(True)
|
||||||
|
@ -286,7 +346,7 @@ class BantrackerTestCase(ChannelPluginTestCase):
|
||||||
# check not pending anymore
|
# check not pending anymore
|
||||||
self.assertFalse(cb.pendingReviews)
|
self.assertFalse(cb.pendingReviews)
|
||||||
|
|
||||||
def testPersistentCache(self):
|
def testReviewStore(self):
|
||||||
"""Save pending reviews and when bans were last checked. This is needed for plugin
|
"""Save pending reviews and when bans were last checked. This is needed for plugin
|
||||||
reloads"""
|
reloads"""
|
||||||
msg1 = ircmsgs.privmsg('nick', 'Hello World')
|
msg1 = ircmsgs.privmsg('nick', 'Hello World')
|
||||||
|
@ -346,5 +406,214 @@ class BantrackerTestCase(ChannelPluginTestCase):
|
||||||
fetch = self.query("SELECT id,channel,mask,operator FROM bans")
|
fetch = self.query("SELECT id,channel,mask,operator FROM bans")
|
||||||
self.assertEqual((1, '#test', 'troll', 'op'), fetch[0])
|
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('')
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue