1
# -*- Encoding: utf-8 -*-
3
# Copyright (c) 2005-2007 Dennis Kaarsemaker
4
# Copyright (c) 2008-2010 Terence Simpson
5
# Copyright (c) 2010 Elián Hanisch
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of version 2 of the GNU General Public License as
9
# published by the Free Software Foundation.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
18
# Based on the standard supybot logging plugin, which has the following
21
# Copyright (c) 2002-2004, Jeremiah Fincher
22
# All rights reserved.
24
# Redistribution and use in source and binary forms, with or without
25
# modification, are permitted provided that the following conditions are met:
27
# * Redistributions of source code must retain the above copyright notice,
28
# this list of conditions, and the following disclaimer.
29
# * Redistributions in binary form must reproduce the above copyright notice,
30
# this list of conditions, and the following disclaimer in the
31
# documentation and/or other materials provided with the distribution.
32
# * Neither the name of the author of this software nor the name of
33
# contributors to this software may be used to endorse or promote products
34
# derived from this software without specific prior written consent.
36
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
37
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
38
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
40
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
41
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
42
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
44
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
46
# POSSIBILITY OF SUCH DAMAGE.
49
from supybot.commands import *
50
import supybot.ircutils as ircutils
51
import supybot.callbacks as callbacks
52
import supybot.ircmsgs as ircmsgs
53
import supybot.conf as conf
54
import supybot.ircdb as ircdb
55
import supybot.schedule as schedule
56
from fnmatch import fnmatch
66
isUserHostmask = ircutils.isUserHostmask
71
return cPickle.dumps(datetime.datetime.now(pytz.timezone(tz)))
74
return cPickle.dumps(datetime.datetime(*time.gmtime(x)[:6], **{'tzinfo': pytz.timezone("UTC")}))
76
def capab(user, capability):
77
capability = capability.lower()
78
capabilities = list(user.capabilities)
79
# Capability hierarchy #
80
if capability == "bantracker":
81
if capab(user, "admin"):
83
if capability == "admin":
84
if capab(user, "owner"):
87
if capability in capabilities:
92
def hostmaskPatternEqual(pattern, hostmask):
93
if pattern.count('!') != 1 or pattern.count('@') != 1:
95
if pattern.count('$') == 1:
96
pattern = pattern.split('$',1)[0]
97
if pattern.startswith('%'):
99
return ircutils.hostmaskPatternEqual(pattern, hostmask)
101
def nickMatch(nick, pattern):
102
"""Checks if a given nick matches a pattern or in a list of patterns."""
103
if isinstance(pattern, str):
107
if fnmatch(nick, s.lower()):
111
def dequeue(parent, irc):
113
queue.dequeue(parent, irc)
115
class MsgQueue(object):
118
def queue(self, msg):
119
if msg not in self.msgcache:
120
self.msgcache.append(msg)
123
def dequeue(self, parent, irc):
124
parent.thread_timer.cancel()
125
parent.thread_timer = threading.Timer(10.0, dequeue, args=(parent, irc))
126
if len(self.msgcache):
127
msg = self.msgcache.pop(0)
129
parent.thread_timer.start()
135
def __init__(self, args=None, **kwargs):
138
# in most ircd: args = (nick, channel, mask, who, when)
141
self.when = float(args[4])
143
self.mask = kwargs['mask']
144
self.who = kwargs['who']
145
self.when = float(kwargs['when'])
147
self.id = kwargs['id']
148
self.ascwhen = time.asctime(time.gmtime(self.when))
151
return (self.mask, self.who, self.ascwhen)
154
return self.__tuple__().__iter__()
157
return "%s by %s on %s" % tuple(self)
160
return '<%s object "%s" at 0x%x>' % (self.__class__.__name__, self, id(self))
163
return self.mask.split('!')[0]
166
return datetime.datetime.fromtimestamp(self.when)
168
def guessBanType(mask):
171
elif ircutils.isUserHostmask(mask) or mask.endswith('(realname)'):
175
class PersistentCache(dict):
176
def __init__(self, filename):
177
self.filename = conf.supybot.directories.data.dirize(filename)
183
reader = csv.reader(open(self.filename, 'rb'))
186
self.time = int(reader.next()[1])
188
host, value = self.deserialize(*row)
199
writer = csv.writer(open(self.filename, 'wb'))
202
writer.writerow(('time', str(int(self.time))))
203
for host, values in self.iteritems():
205
writer.writerow(self.serialize(host, v))
207
def deserialize(self, host, nick, command, channel, text):
208
if command == 'PRIVMSG':
209
msg = ircmsgs.privmsg(channel, text)
210
elif command == 'NOTICE':
211
msg = ircmsgs.notice(channel, text)
214
return (host, (nick, msg))
216
def serialize(self, host, value):
218
command, channel, text = msg.command, msg.args[0], msg.args[1]
219
return (host, nick, command, channel, text)
223
class Bantracker(callbacks.Plugin):
224
"""Plugin to manage bans.
225
See '@list Bantracker' and '@help <command>' for commands"""
229
def __init__(self, irc):
230
self.__parent = super(Bantracker, self)
231
self.__parent.__init__(irc)
232
self.default_irc = irc
236
self.logs = ircutils.IrcDict()
239
self.bans = ircutils.IrcDict()
241
self.thread_timer = threading.Timer(10.0, dequeue, args=(self,irc))
242
self.thread_timer.start()
244
db = self.registryValue('database')
246
self.db = sqlite.connect(db)
251
self.pendingReviews = PersistentCache('bt.reviews.db')
252
self.pendingReviews.open()
253
# add scheduled event for check bans that need review, check every hour
255
schedule.removeEvent(self.name())
258
schedule.addPeriodicEvent(lambda : self.reviewBans(irc), 60*60,
261
def get_nicks(self, irc):
263
for (channel, c) in irc.state.channels.iteritems():
264
if not self.registryValue('enabled', channel):
266
for nick in list(c.users):
268
if not nick in self.nicks:
269
host = self.nick_to_host(irc, nick, False).lower()
270
self.nicks[nick] = host
271
host = host.split('@', 1)[1]
273
if host not in self.hosts:
274
self.hosts[host] = []
275
self.hosts[host].append(nick)
277
def get_bans(self, irc):
279
for channel in irc.state.channels.keys():
280
if not self.registryValue('enabled', channel):
282
if channel not in self.bans:
283
self.bans[channel] = []
284
queue.queue(ircmsgs.mode(channel, 'b'))
286
def sendWhois(self, irc, nick, do_reply=False, *args):
288
irc.queueMsg(ircmsgs.whois(nick, nick))
290
self.replies[nick] = [args[0], args[1:]]
292
def do311(self, irc, msg):
294
nick = msg.args[1].lower()
295
mask = "%s!%s@%s" % (nick, msg.args[2].lower(), msg.args[3].lower())
296
self.nicks[nick] = mask
297
if nick in self.replies:
298
f = getattr(self, "%s_real" % self.replies[nick][0])
299
args = self.replies[nick][1]
300
del self.replies[nick]
301
kwargs={'from_reply': True, 'reply': "%s!%s@%s" % (msg.args[1], msg.args[2], msg.args[3])}
304
def do314(self, irc, msg):
306
nick = msg.args[1].lower()
307
mask = "%s!%s@%s" % (nick, msg.args[2].lower(), msg.args[3].lower())
308
if not nick in self.nicks:
309
self.nicks[nick] = mask
310
if nick in self.replies:
311
f = getattr(self, "%s_real" % self.replies[nick][0])
312
args = self.replies[nick][1]
313
del self.replies[nick]
314
kwargs={'from_reply': True, 'reply': "%s!%s@%s" % (msg.args[1], msg.args[2], msg.args[3])}
317
def do401(self, irc, msg):
319
irc.queueMsg(ircmsgs.IrcMsg(prefix="", command='WHOWAS', args=(msg.args[1],), msg=msg))
321
def do406(self, irc, msg):
323
nick = msg.args[1].lower()
324
if nick in self.replies:
325
f = getattr(self, "%s_real" % self.replies[nick][0])
326
args = self.replies[nick][1]
327
del self.replies[nick]
328
kwargs = {'from_reply': True, 'reply': None}
331
def do367(self, irc, msg):
333
if msg.args[1] not in self.bans.keys():
334
self.bans[msg.args[1]] = []
335
bans = self.bans[msg.args[1]]
336
bans.append(Ban(msg.args))
337
bans.sort(key=lambda x: x.when) # needed for self.reviewBans
339
def nick_to_host(self, irc=None, target='', with_nick=True, reply_now=True):
340
target = target.lower()
341
if ircutils.isUserHostmask(target):
343
elif target in self.nicks:
344
return self.nicks[target]
347
return irc.state.nickToHostmask(target)
351
return "%s!*@*" % target
355
if target in self.nicks:
356
return self.nicks[target]
358
return "%s!*@*" % target
368
self.thread_timer.cancel()
372
schedule.removeEvent(self.name())
373
self.pendingReviews.close()
384
self.lastMsgs.clear()
385
self.lastStates.clear()
388
def __call__(self, irc, msg):
390
super(self.__class__, self).__call__(irc, msg)
391
if irc in self.lastMsgs:
392
if irc not in self.lastStates:
393
self.lastStates[irc] = irc.state.copy()
394
self.lastStates[irc].addMsg(irc, self.lastMsgs[irc])
396
self.lastMsgs[irc] = msg
398
def db_run(self, query, parms, expect_result = False, expect_id = False):
399
if not self.db or self.db.closed:
400
db = self.registryValue('database')
403
self.db = sqlite.connect(db)
405
self.log.error("Bantracker: failed to connect to database")
408
self.log.error("Bantracker: no database")
411
cur = self.db.cursor()
412
cur.execute(query, parms)
414
self.log.error("Bantracker: Error while trying to access the Bantracker database.")
417
if expect_result and cur: data = cur.fetchall()
418
if expect_id: data = self.db.insert_id()
422
def requestComment(self, irc, channel, ban):
423
if not ban or not self.registryValue('request', channel):
425
# check the type of the action taken
427
type = guessBanType(mask)
430
# check if type is enabled
431
if type not in self.registryValue('request.type', channel):
433
prefix = conf.supybot.reply.whenAddressedBy.chars()[0] # prefix char for commands
434
# check to who send the request
436
nick = ircutils.nickFromHostmask(ban.who)
439
if nickMatch(nick, self.registryValue('request.ignore', channel)):
441
if nickMatch(nick, self.registryValue('request.forward', channel)):
442
s = "Please somebody comment on the %s of %s in %s done by %s, use:"\
443
" %scomment %s <comment>" %(type, mask, channel, nick, prefix, ban.id)
444
self._sendForward(irc, s, channel)
447
s = "Please comment on the %s of %s in %s, use: %scomment %s <comment>" \
448
%(type, mask, channel, prefix, ban.id)
449
irc.reply(s, to=nick, private=True)
451
def reviewBans(self, irc=None):
452
reviewTime = int(self.registryValue('request.review') * 86400)
454
# time is zero, do nothing
456
now = time.mktime(time.gmtime())
457
lastreview = self.pendingReviews.time
458
self.pendingReviews.time = now # update last time reviewed
460
# initialize last time reviewed timestamp
461
lastreview = now - reviewTime
463
for channel, bans in self.bans.iteritems():
464
if not self.registryValue('enabled', channel) \
465
or not self.registryValue('request', channel):
469
if guessBanType(ban.mask) in ('quiet', 'removal'):
470
# skip mutes and kicks
472
banAge = now - ban.when
473
reviewWindow = lastreview - ban.when
474
#self.log.debug('review ban: %s ban %s by %s (%s/%s/%s %s)', channel, ban.mask,
475
# ban.who, reviewWindow, reviewTime, banAge, reviewTime - reviewWindow)
476
if reviewWindow <= reviewTime < banAge:
477
# ban is old enough, and inside the "review window"
479
# ban.who should be a user hostmask
480
nick = ircutils.nickFromHostmask(ban.who)
481
host = ircutils.hostFromHostmask(ban.who)
483
if ircutils.isNick(ban.who, strictRfc=True):
484
# ok, op's nick, use it
488
# probably a ban restored by IRC server in a netsplit
489
# XXX see if something can be done about this
491
if nickMatch(nick, self.registryValue('request.ignore', channel)):
495
ban.id = self.get_banId(ban.mask, channel)
496
if nickMatch(nick, self.registryValue('request.forward', channel)):
497
s = "Hi, please somebody review the ban '%s' set by %s on %s in"\
498
" %s, link: %s/bans.cgi?log=%s" %(ban.mask, nick, ban.ascwhen, channel,
499
self.registryValue('bansite'), ban.id)
500
self._sendForward(irc, s, channel)
502
s = "Hi, please review the ban '%s' that you set on %s in %s, link:"\
503
" %s/bans.cgi?log=%s" %(ban.mask, ban.ascwhen, channel,
504
self.registryValue('bansite'), ban.id)
505
msg = ircmsgs.privmsg(nick, s)
506
if host in self.pendingReviews \
507
and (nick, msg) not in self.pendingReviews[host]:
508
self.pendingReviews[host].append((nick, msg))
510
self.pendingReviews[host] = [(nick, msg)]
511
elif banAge < reviewTime:
512
# since we made sure bans are sorted by time, the bans left are more recent
515
def _sendForward(self, irc, s, channel=None):
518
for chan in self.registryValue('request.forward.channels', channel=channel):
519
msg = ircmsgs.notice(chan, s)
522
def _sendReviews(self, irc, msg):
523
host = ircutils.hostFromHostmask(msg.prefix)
524
if host in self.pendingReviews:
525
for nick, m in self.pendingReviews[host]:
526
if msg.nick != nick and not irc.isChannel(nick): # I'm a bit extra careful here
527
# correct nick in msg
528
m = ircmsgs.privmsg(msg.nick, m.args[1])
530
del self.pendingReviews[host]
531
# check if we have any reviews by nick to send
532
if None in self.pendingReviews:
533
L = self.pendingReviews[None]
534
for i, v in enumerate(L):
536
if ircutils.strEqual(msg.nick, nick):
540
del self.pendingReviews[None]
542
def doLog(self, irc, channel, s):
543
if not self.registryValue('enabled', channel):
545
channel = ircutils.toLower(channel)
546
if channel not in self.logs.keys():
547
self.logs[channel] = []
548
format = conf.supybot.log.timestampFormat()
550
s = time.strftime(format, time.gmtime()) + " " + ircutils.stripFormatting(s)
551
self.logs[channel] = self.logs[channel][-199:] + [s.strip()]
553
def doKickban(self, irc, channel, *args, **kwargs):
554
ban = self._doKickban(irc, channel, *args, **kwargs)
555
self.requestComment(irc, channel, ban)
558
def _doKickban(self, irc, channel, operator, target, kickmsg = None, use_time = None, extra_comment = None):
559
if not self.registryValue('enabled', channel):
563
n = fromTime(use_time)
565
nick = ircutils.nickFromHostmask(operator)
568
id = self.db_run("INSERT INTO bans (channel, mask, operator, time, log) values(%s, %s, %s, %s, %s)",
569
(channel, target, nick, n, '\n'.join(self.logs[channel])), expect_id=True)
570
if kickmsg and id and not (kickmsg == nick):
571
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n))
573
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, extra_comment, n))
574
if channel not in self.bans:
575
self.bans[channel] = []
576
ban = Ban(mask=target, who=operator, when=time.mktime(time.gmtime()), id=id)
577
self.bans[channel].append(ban)
580
def doUnban(self, irc, channel, nick, mask):
581
if not self.registryValue('enabled', channel):
583
data = self.db_run("SELECT MAX(id) FROM bans where channel=%s and mask=%s", (channel, mask), expect_result=True)
584
if len(data) and not (data[0][0] == None):
585
self.db_run("UPDATE bans SET removal=%s , removal_op=%s WHERE id=%s", (now(), nick, int(data[0][0])))
586
if not channel in self.bans:
587
self.bans[channel] = []
589
for ban in self.bans[channel]:
591
idx = self.bans[channel].index(ban)
594
del self.bans[channel][idx]
596
def doPrivmsg(self, irc, msg):
597
(recipients, text) = msg.args
598
for channel in recipients.split(','):
599
if irc.isChannel(channel):
600
nick = msg.nick or irc.nick
601
if ircmsgs.isAction(msg):
602
self.doLog(irc, channel,
603
'* %s %s\n' % (nick, ircmsgs.unAction(msg)))
605
self.doLog(irc, channel, '<%s> %s\n' % (nick, text))
606
self._sendReviews(irc, msg)
608
def doNotice(self, irc, msg):
609
(recipients, text) = msg.args
610
for channel in recipients.split(','):
611
if irc.isChannel(channel):
612
self.doLog(irc, channel, '-%s- %s\n' % (msg.nick, text))
614
def doNick(self, irc, msg):
616
newNick = msg.args[0]
617
for (channel, c) in irc.state.channels.iteritems():
618
if newNick in c.users:
619
self.doLog(irc, channel,
620
'*** %s is now known as %s\n' % (oldNick, newNick))
621
if oldNick.lower() in self.nicks:
622
del self.nicks[oldNick.lower()]
623
nick = newNick.lower()
624
hostmask = nick + "!".join(msg.prefix.lower().split('!')[1:])
625
self.nicks[nick] = hostmask
627
def doJoin(self, irc, msg):
629
for channel in msg.args[0].split(','):
631
self.doLog(irc, channel,
632
'*** %s (%s) has joined %s\n' % (msg.nick, msg.prefix.split('!', 1)[1], channel))
634
self.doLog(irc, channel,
635
'*** %s has joined %s\n' % (msg.prefix, channel))
636
if not channel in self.bans.keys():
637
self.bans[channel] = []
638
if msg.prefix.split('!', 1)[0] == irc.nick:
639
queue.queue(ircmsgs.mode(channel, 'b'))
640
nick = msg.nick.lower() or msg.prefix.lower().split('!', 1)[0]
641
self.nicks[nick] = msg.prefix.lower()
643
def doKick(self, irc, msg):
644
if len(msg.args) == 3:
645
(channel, target, kickmsg) = msg.args
647
(channel, target) = msg.args
649
host = self.nick_to_host(irc, target, True)
650
if host == "%s!*@*" % host:
653
self.doLog(irc, channel,
654
'*** %s was kicked by %s (%s)\n' % (target, msg.nick, kickmsg))
656
self.doLog(irc, channel,
657
'*** %s was kicked by %s\n' % (target, msg.nick))
658
self.doKickban(irc, channel, msg.prefix, target, kickmsg, extra_comment=host)
660
def doPart(self, irc, msg):
661
for channel in msg.args[0].split(','):
662
self.doLog(irc, channel, '*** %s (%s) has left %s (%s)\n' % (msg.nick, msg.prefix, channel, len(msg.args) > 1 and msg.args[1] or ''))
663
if len(msg.args) > 1 and msg.args[1].startswith('requested by'):
664
args = msg.args[1].split()
665
self.doKickban(irc, channel, args[2], msg.nick, ' '.join(args[3:]).strip(), extra_comment=msg.prefix)
667
def doMode(self, irc, msg):
668
channel = msg.args[0]
669
if irc.isChannel(channel) and msg.args[1:]:
670
self.doLog(irc, channel,
671
'*** %s sets mode: %s %s\n' %
672
(msg.nick or msg.prefix, msg.args[1],
673
' '.join(msg.args[2:])))
674
modes = ircutils.separateModes(msg.args[1:])
680
if param[0] not in ("+b", "-b", "+q", "-q"):
683
if mask.startswith("$r:"):
685
realname = ' (realname)'
687
if param[0][1] == 'q':
690
if param[0] in ('+b', '+q'):
691
comment = self.getHostFromBan(irc, msg, mask)
692
self.doKickban(irc, channel, msg.prefix, mask + realname, extra_comment=comment)
693
elif param[0] in ('-b', '-q'):
694
self.doUnban(irc,channel, msg.nick, mask + realname)
696
def getHostFromBan(self, irc, msg, mask):
697
if irc not in self.lastStates:
698
self.lastStates[irc] = irc.state.copy()
702
(nick, ident, host) = ircutils.splitHostmask(mask)
703
except AssertionError:
708
if mask[0] not in ('*', '?'): # Nick ban
709
if nick in self.nicks:
710
return self.nicks[nick]
711
else: # Host/ident ban
712
for (inick, ihost) in self.nicks.iteritems():
713
if ircutils.hostmaskPatternEqual(mask, ihost):
717
def doTopic(self, irc, msg):
718
if len(msg.args) == 1:
719
return # It's an empty TOPIC just to get the current topic.
720
channel = msg.args[0]
721
self.doLog(irc, channel,
722
'*** %s changes topic to "%s"\n' % (msg.nick, msg.args[1]))
724
def doQuit(self, irc, msg):
725
if irc not in self.lastStates:
726
self.lastStates[irc] = irc.state.copy()
727
for (channel, chan) in self.lastStates[irc].channels.iteritems():
728
if msg.nick in chan.users:
729
self.doLog(irc, channel, '*** %s (%s) has quit IRC (%s)\n' % (msg.nick, msg.prefix, msg.args[0]))
730
# if msg.nick in self.user:
731
# del self.user[msg.nick]
733
def outFilter(self, irc, msg):
734
# Gotta catch my own messages *somehow* :)
735
# Let's try this little trick...
736
if msg.command in ('PRIVMSG', 'NOTICE'):
737
# Other messages should be sent back to us.
738
m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix)
742
# def callPrecedence(self, irc):
744
# for cb in irc.callbacks:
745
# if cb.name() == 'IRCLogin':
746
# return (['IRCLogin'], [])
749
def check_auth(self, irc, msg, args, cap='bantracker'):
751
for cb in self.callPrecedence(irc)[0]:
752
if cb.name() == "IRCLogin":
754
if hasIRCLogin and not msg.tagged('identified'):
755
irc.error(conf.supybot.replies.incorrectAuthentication())
758
user = ircdb.users.getUser(msg.prefix)
760
irc.error(conf.supybot.replies.incorrectAuthentication())
763
if not capab(user, cap):
764
irc.error(conf.supybot.replies.noCapability() % cap)
768
def btlogin(self, irc, msg, args):
769
"""Takes no arguments
771
Sends you a message with a link to login to the bantracker.
773
user = self.check_auth(irc, msg, args)
776
user.addAuth(msg.prefix)
778
ircdb.users.setUser(user, flush=False)
782
if not capab(user, 'bantracker'):
783
irc.error(conf.supybot.replies.noCapability() % 'bantracker')
785
if not self.registryValue('bansite'):
786
irc.error("No bansite set, please set supybot.plugins.Bantracker.bansite")
788
sessid = hashlib.md5('%s%s%d' % (msg.prefix, time.time(), random.randint(1,100000))).hexdigest()
789
self.db_run("INSERT INTO sessions (session_id, user, time) VALUES (%s, %s, %d);",
790
( sessid, msg.nick, int(time.mktime(time.gmtime())) ) )
791
irc.reply('Log in at %s/bans.cgi?sess=%s' % (self.registryValue('bansite'), sessid), private=True)
793
btlogin = wrap(btlogin)
795
def mark(self, irc, msg, args, channel, target, kickmsg):
796
"""[<channel>] <nick|hostmask> [<comment>]
798
Creates an entry in the Bantracker as if <nick|hostmask> was kicked from <channel> with the comment <comment>,
799
if <comment> is given it will be uses as the comment on the Bantracker, <channel> is only needed when send in /msg
801
user = self.check_auth(irc, msg, args)
805
if target == '*' or target[0] == '*':
806
irc.error("Can not create a mark for '%s'" % target)
810
irc.error('<channel> must be given if not in a channel')
812
channel = channel.lower()
814
for chan in irc.state.channels.keys():
815
channels.append(chan.lower())
817
if not channel in channels:
818
irc.error('Not in that channel')
824
kickmsg = "**MARK** - %s" % kickmsg
825
hostmask = self.nick_to_host(irc, target)
827
self.doLog(irc, channel.lower(), '*** %s requested a mark for %s\n' % (msg.nick, target))
828
self._doKickban(irc, channel.lower(), msg.prefix, hostmask, kickmsg)
831
mark = wrap(mark, [optional('channel'), 'something', additional('text')])
833
def sort_bans(self, channel=None):
834
data = self.db_run("SELECT mask, removal, channel, id FROM bans", (), expect_result=True)
836
data = [i for i in data if i[2] == channel]
837
bans = [(i[0], i[3]) for i in data if i[1] == None and '%' not in i[0]]
838
mutes = [(i[0], i[3]) for i in data if i[1] == None and '%' in i[0]]
841
def get_banId(self, mask, channel):
842
data = self.db_run("SELECT MAX(id) FROM bans WHERE mask=%s AND channel=%s", (mask, channel), True)
845
if not data or not data[0]:
849
def getBans(self, hostmask, channel):
852
if channel in self.bans and self.bans[channel]:
853
for b in self.bans[channel]:
854
if hostmaskPatternEqual(b.mask, hostmask):
855
match.append((b.mask, self.get_banId(b.mask,channel)))
856
data = self.sort_bans(channel)
858
if hostmaskPatternEqual(e[0], hostmask):
859
if (e[0], e[1]) not in match:
860
match.append((e[0], e[1]))
863
for b in self.bans[c]:
864
if hostmaskPatternEqual(b.mask, hostmask):
865
match.append((b.mask, self.get_banId(b.mask,c)))
866
data = self.sort_bans()
868
if hostmaskPatternEqual(e[0], hostmask):
869
if (e[0], e[1]) not in match:
870
match.append((e[0], e[1]))
873
def bansearch_real(self, irc, msg, args, target, channel, from_reply=False, reply=None):
874
"""<nick|hostmask> [<channel>]
876
Search bans database for a ban on <nick|hostmask>,
877
if <channel> is not given search all channel bans.
879
def format_entry(entry):
880
ret = list(entry[:-1])
881
t = cPickle.loads(entry[-1]).astimezone(pytz.timezone('UTC')).strftime("%b %d %Y %H:%M:%S")
885
user = self.check_auth(irc, msg, args)
891
if capab(user, 'admin'):
892
if len(queue.msgcache) > 0:
893
irc.reply("Warning: still syncing (%i)" % len(queue.msgcache))
894
irc.reply("No matches found for %s in %s" % (hostmask, True and channel or "any channel"))
897
hostmask = self.nick_to_host(irc, target, reply_now=False)
899
self.sendWhois(irc, target, True, 'bansearch', irc, msg, args, target, channel)
901
match = self.getBans(hostmask, channel)
903
if capab(user, 'owner'):
904
if len(queue.msgcache) > 0:
905
irc.reply("Warning: still syncing (%i)" % len(queue.msgcache))
908
if not ircutils.isChannel(channel):
911
if '*' in target or '?' in target:
912
irc.error("Can only search for a complete hostmask")
915
if '!' not in target or '@' not in target:
916
hostmask = self.nick_to_host(irc, target)
917
if '!' not in hostmask:
919
hostmask = hostmask.replace("n=", "!n=", 1)
920
elif "i=" in hostmask:
921
hostmask = hostmask.replace("i=", "!i=", 1)
922
match = self.getBans(hostmask, channel)
925
irc.reply("No matches found for %s in %s" % (hostmask, True and channel or "any channel"))
931
ret.append((format_entry(self.db_run("SELECT mask, operator, channel, time FROM bans WHERE id=%d", m[1], expect_result=True)[0]), m[1]))
935
for b in self.bans[c]:
939
irc.reply("Match %s in %s" % (b, c))
943
if '*' in i[0][0] or '?' in i[0][0]:
944
banstr = "Match: %s by %s in %s on %s (ID: %s)" % (i[0] + (i[1],))
946
banstr = "Mark: by %s in %s on %s (ID: %s)" % (i[0][1:] + (i[1],))
947
if (banstr, False) not in replies:
948
replies.append((banstr, False))
952
irc.reply(r[0], private=r[1])
954
irc.error("Something not so good happened, please tell stdin about it")
956
bansearch = wrap(bansearch_real, ['something', optional('something', default=None)])
958
def banlog(self, irc, msg, args, target, channel):
959
"""<nick|hostmask> [<channel>]
961
Prints the last 5 messages from the nick/host logged before a ban/mute,
962
the nick/host has to have an active ban/mute against it.
963
If channel is not given search all channel bans.
965
user = self.check_auth(irc, msg, args)
969
if capab(user, 'owner') and len(queue.msgcache) > 0:
970
irc.reply("Warning: still syncing (%i)" % len(queue.msgcache))
972
hostmask = self.nick_to_host(irc, target)
973
target = target.split('!', 1)[0]
974
match = self.getBans(hostmask, channel)
977
irc.reply("No matches found for %s (%s) in %s" % (target, hostmask, True and channel or "any channel"))
983
ret.append((self.db_run("SELECT log, channel FROM bans WHERE id=%d", m[1], expect_result=True), m[1]))
987
irc.reply("No matches in tracker")
991
lines = ["%s: %s" % (log[0][1], i) for i in log[0][0].split('\n') if "<%s>" % target.lower() in i.lower() and i[21:21+len(target)].lower() == target.lower()]
995
irc.error("No log for ID %s available" % id)
1005
banlog = wrap(banlog, ['something', optional('anything', default=None)])
1007
def updatebt(self, irc, msg, args, channel):
1010
Update bans in the tracker from the channel ban list,
1011
if channel is not given then run in all channels
1015
data = self.db_run("SELECT mask, removal FROM bans WHERE channel=%s", chan, expect_result=True)
1017
for mask, removal in data:
1018
if removal is not None:
1020
elif not isUserHostmask(mask) and mask[0] != '$':
1026
bans = getBans(chan)
1028
new_bans = [i.mask for i in self.bans[chan]]
1030
for ban in old_bans:
1031
if ban not in new_bans:
1032
remove_bans.append(ban)
1035
for ban in remove_bans:
1036
self.log.info("Bantracker: Removing ban %s from %s" % (ban.replace('%', '%%'), chan))
1037
self.doUnban(irc, channel, "Automated-Removal", ban)
1039
return len(remove_bans)
1042
bans = self.bans[chan]
1043
old_bans = getBans(chan)
1046
if ban.mask not in old_bans and ban not in add_bans:
1047
add_bans.append(ban)
1049
for ban in add_bans:
1051
if nick.endswith('.freenode.net'):
1052
nick = "Automated-Addition"
1053
self.log.info("Bantracker: Adding ban %s to %s (%s)" % (str(ban).replace('%', '%%'), chan, nick))
1054
self.doLog(irc, channel.lower(), '*** Ban sync from channel: %s\n' % str(ban).replace('%', '%%'))
1055
self._doKickban(irc, chan, nick, ban.mask, use_time = ban.when)
1056
return len(add_bans)
1058
if not self.check_auth(irc, msg, args, 'owner'):
1064
if len(queue.msgcache) > 0:
1065
irc.reply("Error: still syncing (%i)" % len(queue.msgcache))
1070
rem_res += remBans(channel)
1071
add_res += addBans(channel)
1073
for channel in irc.state.channels.keys():
1074
if channel not in self.bans:
1075
self.bans[channel] = []
1076
rem_res += remBans(channel)
1077
add_res += addBans(channel)
1079
irc.error("%s, Please wait longer" % e)
1082
irc.reply("Cleared %i obsolete bans, Added %i new bans" % (rem_res, add_res))
1084
updatebt = wrap(updatebt, [optional('anything', default=None)])
1086
def comment(self, irc, msg, args, id, kickmsg):
1089
Reads or adds the <comment> for the ban with <id>,
1090
use @bansearch to find the id of a ban
1092
def addComment(id, nick, msg):
1094
self.db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, msg, n))
1095
def readComment(id):
1096
return self.db_run("SELECT who, comment, time FROM comments WHERE ban_id=%i", (id,), True)
1100
addComment(id, nick, kickmsg)
1103
data = readComment(id)
1106
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()) )
1108
irc.error("No comments recorded for ban %i" % id)
1109
comment = wrap(comment, ['id', optional('text')])
1111
def banlink(self, irc, msg, args, id, highlight):
1112
"""<id> [<highlight>]
1114
Returns a link to the log of the ban/kick with id <id>.
1115
If <highlight> is given, lines containing that term will be highlighted
1117
if not self.check_auth(irc, msg, args):
1120
irc.reply("%s/bans.cgi?log=%s" % (self.registryValue('bansite'), id), private=True)
1122
irc.reply("%s/bans.cgi?log=%s&mark=%s" % (self.registryValue('bansite'), id, highlight), private=True)
1123
banlink = wrap(banlink, ['id', optional('somethingWithoutSpaces')])
1125
def banreview(self, irc, msg, args):
1127
Lists pending ban reviews."""
1128
if not self.check_auth(irc, msg, args):
1131
for reviews in self.pendingReviews.itervalues():
1132
for nick, msg in reviews:
1137
total = sum(count.itervalues())
1138
s = ' '.join([ '%s:%s' %pair for pair in count.iteritems() ])
1139
s = 'Pending ban reviews (%s): %s' %(total, s)
1142
banreview = wrap(banreview)