~ubuntu-ru-irc/+junk/irckit

« back to all changes in this revision

Viewing changes to ubuntuhelp/ubuntubot/plugins/Bantracker/plugin.py

  • Committer: rmPIC30 at gmail
  • Date: 2010-07-13 09:08:58 UTC
  • Revision ID: rmpic30@gmail.com-20100713090858-w5kkmk093hx38cen
Добавляю бота

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Encoding: utf-8 -*-
 
2
###
 
3
# Copyright (c) 2005-2007 Dennis Kaarsemaker
 
4
# Copyright (c) 2008-2010 Terence Simpson
 
5
# Copyright (c) 2010 Elián Hanisch
 
6
#
 
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.
 
10
#
 
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.
 
15
#
 
16
###
 
17
###
 
18
# Based on the standard supybot logging plugin, which has the following
 
19
# copyright:
 
20
#
 
21
# Copyright (c) 2002-2004, Jeremiah Fincher
 
22
# All rights reserved.
 
23
#
 
24
# Redistribution and use in source and binary forms, with or without
 
25
# modification, are permitted provided that the following conditions are met:
 
26
#
 
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.
 
35
#
 
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.
 
47
###
 
48
 
 
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
 
57
import sqlite
 
58
import pytz
 
59
import cPickle
 
60
import datetime
 
61
import time
 
62
import random
 
63
import hashlib
 
64
import threading
 
65
 
 
66
isUserHostmask = ircutils.isUserHostmask
 
67
 
 
68
tz = 'UTC'
 
69
 
 
70
def now():
 
71
    return cPickle.dumps(datetime.datetime.now(pytz.timezone(tz)))
 
72
 
 
73
def fromTime(x):
 
74
    return cPickle.dumps(datetime.datetime(*time.gmtime(x)[:6], **{'tzinfo': pytz.timezone("UTC")}))
 
75
 
 
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"):
 
82
            return True
 
83
    if capability == "admin":
 
84
        if capab(user, "owner"):
 
85
            return True
 
86
    # End #
 
87
    if capability in capabilities:
 
88
        return True
 
89
    else:
 
90
        return False
 
91
 
 
92
def hostmaskPatternEqual(pattern, hostmask):
 
93
    if pattern.count('!') != 1 or pattern.count('@') != 1:
 
94
        return False
 
95
    if pattern.count('$') == 1:
 
96
        pattern = pattern.split('$',1)[0]
 
97
    if pattern.startswith('%'):
 
98
        pattern = pattern[1:]
 
99
    return ircutils.hostmaskPatternEqual(pattern, hostmask)
 
100
 
 
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):
 
104
        pattern = [pattern]
 
105
    nick = nick.lower()
 
106
    for s in pattern:
 
107
        if fnmatch(nick, s.lower()):
 
108
            return True
 
109
    return False
 
110
 
 
111
def dequeue(parent, irc):
 
112
    global queue
 
113
    queue.dequeue(parent, irc)
 
114
 
 
115
class MsgQueue(object):
 
116
    def __init__(self):
 
117
        self.msgcache = []
 
118
    def queue(self, msg):
 
119
        if msg not in self.msgcache:
 
120
            self.msgcache.append(msg)
 
121
    def clear(self):
 
122
        self.msgcache = []
 
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)
 
128
            irc.queueMsg(msg)
 
129
        parent.thread_timer.start()
 
130
 
 
131
queue = MsgQueue()
 
132
 
 
133
class Ban(object):
 
134
    """Hold my bans"""
 
135
    def __init__(self, args=None, **kwargs):
 
136
        self.id = None
 
137
        if args:
 
138
            # in most ircd: args = (nick, channel, mask, who, when)
 
139
            self.mask = args[2]
 
140
            self.who = args[3]
 
141
            self.when = float(args[4])
 
142
        else:
 
143
            self.mask = kwargs['mask']
 
144
            self.who = kwargs['who']
 
145
            self.when = float(kwargs['when'])
 
146
            if 'id' in kwargs:
 
147
                self.id = kwargs['id']
 
148
        self.ascwhen = time.asctime(time.gmtime(self.when))
 
149
 
 
150
    def __tuple__(self):
 
151
        return (self.mask, self.who, self.ascwhen)
 
152
 
 
153
    def __iter__(self):
 
154
        return self.__tuple__().__iter__()
 
155
 
 
156
    def __str__(self):
 
157
        return "%s by %s on %s" % tuple(self)
 
158
 
 
159
    def __repr__(self):
 
160
        return '<%s object "%s" at 0x%x>' % (self.__class__.__name__, self, id(self))
 
161
 
 
162
    def op(self):
 
163
        return self.mask.split('!')[0]
 
164
 
 
165
    def time(self):
 
166
        return datetime.datetime.fromtimestamp(self.when)
 
167
 
 
168
def guessBanType(mask):
 
169
    if mask[0] == '%':
 
170
        return 'quiet'
 
171
    elif ircutils.isUserHostmask(mask) or mask.endswith('(realname)'):
 
172
        return 'ban'
 
173
    return 'removal'
 
174
 
 
175
class PersistentCache(dict):
 
176
    def __init__(self, filename):
 
177
        self.filename = conf.supybot.directories.data.dirize(filename)
 
178
        self.time = 0
 
179
 
 
180
    def open(self):
 
181
        import csv
 
182
        try:
 
183
            reader = csv.reader(open(self.filename, 'rb'))
 
184
        except IOError:
 
185
            return
 
186
        self.time = int(reader.next()[1])
 
187
        for row in reader:
 
188
            host, value = self.deserialize(*row)
 
189
            try:
 
190
                L = self[host]
 
191
                if value not in L:
 
192
                    L.append(value)
 
193
            except KeyError:
 
194
                self[host] = [value]
 
195
 
 
196
    def close(self):
 
197
        import csv
 
198
        try:
 
199
            writer = csv.writer(open(self.filename, 'wb'))
 
200
        except IOError:
 
201
            return
 
202
        writer.writerow(('time', str(int(self.time))))
 
203
        for host, values in self.iteritems():
 
204
            for v in values:
 
205
                writer.writerow(self.serialize(host, v))
 
206
 
 
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)
 
212
        else:
 
213
            return
 
214
        return (host, (nick, msg))
 
215
 
 
216
    def serialize(self, host, value):
 
217
        nick, msg = value
 
218
        command, channel, text = msg.command, msg.args[0], msg.args[1]
 
219
        return (host, nick, command, channel, text)
 
220
 
 
221
 
 
222
 
 
223
class Bantracker(callbacks.Plugin):
 
224
    """Plugin to manage bans.
 
225
       See '@list Bantracker' and '@help <command>' for commands"""
 
226
    noIgnore = True
 
227
    threaded = True
 
228
    
 
229
    def __init__(self, irc):
 
230
        self.__parent = super(Bantracker, self)
 
231
        self.__parent.__init__(irc)
 
232
        self.default_irc = irc
 
233
        self.lastMsgs = {}
 
234
        self.lastStates = {}
 
235
        self.replies = {}
 
236
        self.logs = ircutils.IrcDict()
 
237
        self.nicks = {}
 
238
        self.hosts = {}
 
239
        self.bans = ircutils.IrcDict()
 
240
 
 
241
        self.thread_timer = threading.Timer(10.0, dequeue, args=(self,irc))
 
242
        self.thread_timer.start()
 
243
 
 
244
        db = self.registryValue('database')
 
245
        if db:
 
246
            self.db = sqlite.connect(db)
 
247
        else:
 
248
            self.db = None
 
249
        self.get_bans(irc)
 
250
        self.get_nicks(irc)
 
251
        self.pendingReviews = PersistentCache('bt.reviews.db')
 
252
        self.pendingReviews.open()
 
253
        # add scheduled event for check bans that need review, check every hour
 
254
        try:
 
255
            schedule.removeEvent(self.name())
 
256
        except:
 
257
            pass
 
258
        schedule.addPeriodicEvent(lambda : self.reviewBans(irc), 60*60,
 
259
                name=self.name())
 
260
 
 
261
    def get_nicks(self, irc):
 
262
        self.hosts.clear()
 
263
        for (channel, c) in irc.state.channels.iteritems():
 
264
            if not self.registryValue('enabled', channel):
 
265
                continue
 
266
            for nick in list(c.users):
 
267
                nick = nick.lower()
 
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]
 
272
                    if '*' not in host:
 
273
                        if host not in self.hosts:
 
274
                            self.hosts[host] = []
 
275
                        self.hosts[host].append(nick)
 
276
 
 
277
    def get_bans(self, irc):
 
278
        global queue
 
279
        for channel in irc.state.channels.keys():
 
280
            if not self.registryValue('enabled', channel):
 
281
                continue
 
282
            if channel not in self.bans:
 
283
                self.bans[channel] = []
 
284
            queue.queue(ircmsgs.mode(channel, 'b'))
 
285
 
 
286
    def sendWhois(self, irc, nick, do_reply=False, *args):
 
287
        nick = nick.lower()
 
288
        irc.queueMsg(ircmsgs.whois(nick, nick))
 
289
        if do_reply:
 
290
            self.replies[nick] = [args[0], args[1:]]
 
291
 
 
292
    def do311(self, irc, msg):
 
293
        """/whois"""
 
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])}
 
302
            f(*args, **kwargs)
 
303
 
 
304
    def do314(self, irc, msg):
 
305
        """/whowas"""
 
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])}
 
315
            f(*args, **kwargs)
 
316
 
 
317
    def do401(self, irc, msg):
 
318
        """/whois faild"""
 
319
        irc.queueMsg(ircmsgs.IrcMsg(prefix="", command='WHOWAS', args=(msg.args[1],), msg=msg))
 
320
 
 
321
    def do406(self, irc, msg):
 
322
        """/whowas faild"""
 
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}
 
329
            f(*args, **kwargs)
 
330
 
 
331
    def do367(self, irc, msg):
 
332
        """Got ban"""
 
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
 
338
 
 
339
    def nick_to_host(self, irc=None, target='', with_nick=True, reply_now=True):
 
340
        target = target.lower()
 
341
        if ircutils.isUserHostmask(target):
 
342
            return target
 
343
        elif target in self.nicks:
 
344
            return self.nicks[target]
 
345
        elif irc:
 
346
            try:
 
347
                return irc.state.nickToHostmask(target)
 
348
            except:
 
349
                if reply_now:
 
350
                    if with_nick:
 
351
                        return "%s!*@*" % target
 
352
                    return "*@*"
 
353
        return
 
354
 
 
355
        if target in self.nicks:
 
356
            return self.nicks[target]
 
357
        else:
 
358
            return "%s!*@*" % target
 
359
 
 
360
    def die(self):
 
361
        global queue
 
362
        if self.db:
 
363
            try:
 
364
                self.db.close()
 
365
            except:
 
366
                pass
 
367
        try:
 
368
            self.thread_timer.cancel()
 
369
        except:
 
370
            pass
 
371
        queue.clear()
 
372
        schedule.removeEvent(self.name())
 
373
        self.pendingReviews.close()
 
374
 
 
375
    def reset(self):
 
376
        global queue
 
377
        if self.db:
 
378
            try:
 
379
                self.db.close()
 
380
            except:
 
381
                pass
 
382
        queue.clear()
 
383
#        self.logs.clear()
 
384
        self.lastMsgs.clear()
 
385
        self.lastStates.clear()
 
386
#        self.nicks.clear()
 
387
 
 
388
    def __call__(self, irc, msg):
 
389
        try:
 
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])
 
395
        finally:
 
396
            self.lastMsgs[irc] = msg
 
397
 
 
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')
 
401
            if db:
 
402
                try:
 
403
                    self.db = sqlite.connect(db)
 
404
                except:
 
405
                    self.log.error("Bantracker: failed to connect to database")
 
406
                    return
 
407
            else:
 
408
                self.log.error("Bantracker: no database")
 
409
                return
 
410
        try:
 
411
            cur = self.db.cursor()
 
412
            cur.execute(query, parms)
 
413
        except:
 
414
            self.log.error("Bantracker: Error while trying to access the Bantracker database.")
 
415
            return None
 
416
        data = None
 
417
        if expect_result and cur: data = cur.fetchall()
 
418
        if expect_id: data = self.db.insert_id()
 
419
        self.db.commit()
 
420
        return data
 
421
 
 
422
    def requestComment(self, irc, channel, ban):
 
423
        if not ban or not self.registryValue('request', channel):
 
424
            return
 
425
        # check the type of the action taken
 
426
        mask = ban.mask
 
427
        type = guessBanType(mask)
 
428
        if type == 'quiet':
 
429
            mask = mask[1:]
 
430
        # check if type is enabled
 
431
        if type not in self.registryValue('request.type', channel):
 
432
            return
 
433
        prefix = conf.supybot.reply.whenAddressedBy.chars()[0] # prefix char for commands
 
434
        # check to who send the request
 
435
        try:
 
436
            nick = ircutils.nickFromHostmask(ban.who)
 
437
        except:
 
438
            nick = ban.who
 
439
        if nickMatch(nick, self.registryValue('request.ignore', channel)):
 
440
            return
 
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)
 
445
        else:
 
446
            # send to op
 
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)
 
450
 
 
451
    def reviewBans(self, irc=None):
 
452
        reviewTime = int(self.registryValue('request.review') * 86400)
 
453
        if not reviewTime:
 
454
            # time is zero, do nothing
 
455
            return
 
456
        now = time.mktime(time.gmtime())
 
457
        lastreview = self.pendingReviews.time
 
458
        self.pendingReviews.time = now # update last time reviewed
 
459
        if not lastreview:
 
460
            # initialize last time reviewed timestamp
 
461
            lastreview = now - reviewTime
 
462
 
 
463
        for channel, bans in self.bans.iteritems():
 
464
            if not self.registryValue('enabled', channel) \
 
465
                    or not self.registryValue('request', channel):
 
466
                continue
 
467
 
 
468
            for ban in bans:
 
469
                if guessBanType(ban.mask) in ('quiet', 'removal'):
 
470
                    # skip mutes and kicks
 
471
                    continue
 
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"
 
478
                    try:
 
479
                        # ban.who should be a user hostmask
 
480
                        nick = ircutils.nickFromHostmask(ban.who)
 
481
                        host = ircutils.hostFromHostmask(ban.who)
 
482
                    except:
 
483
                        if ircutils.isNick(ban.who, strictRfc=True):
 
484
                            # ok, op's nick, use it
 
485
                            nick = ban.who
 
486
                            host = None
 
487
                        else:
 
488
                            # probably a ban restored by IRC server in a netsplit
 
489
                            # XXX see if something can be done about this
 
490
                            continue
 
491
                    if nickMatch(nick, self.registryValue('request.ignore', channel)):
 
492
                        # in the ignore list
 
493
                        continue
 
494
                    if not ban.id:
 
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)
 
501
                    else:
 
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))
 
509
                        else:
 
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
 
513
                    break
 
514
 
 
515
    def _sendForward(self, irc, s, channel=None):
 
516
        if not irc:
 
517
            return
 
518
        for chan in self.registryValue('request.forward.channels', channel=channel):
 
519
            msg = ircmsgs.notice(chan, s)
 
520
            irc.queueMsg(msg)
 
521
 
 
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])
 
529
                irc.queueMsg(m)
 
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):
 
535
                nick, m = v
 
536
                if ircutils.strEqual(msg.nick, nick):
 
537
                    irc.queueMsg(m)
 
538
                    del L[i]
 
539
            if not L:
 
540
                del self.pendingReviews[None]
 
541
 
 
542
    def doLog(self, irc, channel, s):
 
543
        if not self.registryValue('enabled', channel):
 
544
            return
 
545
        channel = ircutils.toLower(channel) 
 
546
        if channel not in self.logs.keys():
 
547
            self.logs[channel] = []
 
548
        format = conf.supybot.log.timestampFormat()
 
549
        if format:
 
550
            s = time.strftime(format, time.gmtime()) + " " + ircutils.stripFormatting(s)
 
551
        self.logs[channel] = self.logs[channel][-199:] + [s.strip()]
 
552
 
 
553
    def doKickban(self, irc, channel, *args, **kwargs):
 
554
        ban = self._doKickban(irc, channel, *args, **kwargs)
 
555
        self.requestComment(irc, channel, ban)
 
556
        return ban
 
557
 
 
558
    def _doKickban(self, irc, channel, operator, target, kickmsg = None, use_time = None, extra_comment = None):
 
559
        if not self.registryValue('enabled', channel):
 
560
            return
 
561
        n = now()
 
562
        if use_time:
 
563
            n = fromTime(use_time)
 
564
        try:
 
565
            nick = ircutils.nickFromHostmask(operator)
 
566
        except:
 
567
            nick = 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))
 
572
        if extra_comment:
 
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)
 
578
        return ban
 
579
 
 
580
    def doUnban(self, irc, channel, nick, mask):
 
581
        if not self.registryValue('enabled', channel):
 
582
            return
 
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] = []
 
588
        idx = None
 
589
        for ban in self.bans[channel]:
 
590
            if ban.mask == mask:
 
591
                idx = self.bans[channel].index(ban)
 
592
                break
 
593
        if idx != None:
 
594
            del self.bans[channel][idx]
 
595
 
 
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)))
 
604
                else:
 
605
                    self.doLog(irc, channel, '<%s> %s\n' % (nick, text))
 
606
        self._sendReviews(irc, msg)
 
607
 
 
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))
 
613
 
 
614
    def doNick(self, irc, msg):
 
615
        oldNick = msg.nick
 
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
 
626
 
 
627
    def doJoin(self, irc, msg):
 
628
        global queue
 
629
        for channel in msg.args[0].split(','):
 
630
            if msg.nick:
 
631
                self.doLog(irc, channel,
 
632
                       '*** %s (%s) has joined %s\n' % (msg.nick, msg.prefix.split('!', 1)[1], channel))
 
633
            else:
 
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()
 
642
 
 
643
    def doKick(self, irc, msg):
 
644
        if len(msg.args) == 3:
 
645
            (channel, target, kickmsg) = msg.args
 
646
        else:
 
647
            (channel, target) = msg.args
 
648
            kickmsg = ''
 
649
        host = self.nick_to_host(irc, target, True)
 
650
        if host == "%s!*@*" % host:
 
651
            host = None
 
652
        if kickmsg:
 
653
            self.doLog(irc, channel,
 
654
                       '*** %s was kicked by %s (%s)\n' % (target, msg.nick, kickmsg))
 
655
        else:
 
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)
 
659
 
 
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)
 
666
 
 
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:])
 
675
            for param in modes:
 
676
                realname = ''
 
677
                mode = param[0]
 
678
                mask = ''
 
679
                comment=None
 
680
                if param[0] not in ("+b", "-b", "+q", "-q"):
 
681
                    continue
 
682
                mask = param[1]
 
683
                if mask.startswith("$r:"):
 
684
                    mask = mask[3:]
 
685
                    realname = ' (realname)'
 
686
 
 
687
                if param[0][1] == 'q':
 
688
                    mask = '%' + mask
 
689
 
 
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)
 
695
 
 
696
    def getHostFromBan(self, irc, msg, mask):
 
697
        if irc not in self.lastStates:
 
698
            self.lastStates[irc] = irc.state.copy()
 
699
        if mask[0] == '%':
 
700
            mask = mask[1:]
 
701
        try:
 
702
            (nick, ident, host) = ircutils.splitHostmask(mask)
 
703
        except AssertionError:
 
704
            # not a hostmask
 
705
            return None
 
706
        channel = None
 
707
        chan = None
 
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):
 
714
                    return ihost
 
715
        return None
 
716
 
 
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]))
 
723
 
 
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]
 
732
 
 
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)
 
739
            self(irc, m)
 
740
        return msg
 
741
        
 
742
#    def callPrecedence(self, irc):
 
743
#        before = []
 
744
#        for cb in irc.callbacks:
 
745
#            if cb.name() == 'IRCLogin':
 
746
#                return (['IRCLogin'], [])
 
747
#        return ([], [])
 
748
 
 
749
    def check_auth(self, irc, msg, args, cap='bantracker'):
 
750
        hasIRCLogin = False
 
751
        for cb in self.callPrecedence(irc)[0]:
 
752
            if cb.name() == "IRCLogin":
 
753
                hasIRCLogin = True
 
754
        if hasIRCLogin and not msg.tagged('identified'):
 
755
            irc.error(conf.supybot.replies.incorrectAuthentication())
 
756
            return False
 
757
        try:
 
758
            user = ircdb.users.getUser(msg.prefix)
 
759
        except:
 
760
            irc.error(conf.supybot.replies.incorrectAuthentication())
 
761
            return False
 
762
 
 
763
        if not capab(user, cap):
 
764
            irc.error(conf.supybot.replies.noCapability() % cap)
 
765
            return False
 
766
        return user
 
767
 
 
768
    def btlogin(self, irc, msg, args):
 
769
        """Takes no arguments
 
770
 
 
771
        Sends you a message with a link to login to the bantracker.
 
772
        """
 
773
        user = self.check_auth(irc, msg, args)
 
774
        if not user:
 
775
            return
 
776
        user.addAuth(msg.prefix)
 
777
        try:
 
778
            ircdb.users.setUser(user, flush=False)
 
779
        except:
 
780
            pass
 
781
 
 
782
        if not capab(user, 'bantracker'):
 
783
            irc.error(conf.supybot.replies.noCapability() % 'bantracker')
 
784
            return
 
785
        if not self.registryValue('bansite'):
 
786
            irc.error("No bansite set, please set supybot.plugins.Bantracker.bansite")
 
787
            return
 
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)
 
792
 
 
793
    btlogin = wrap(btlogin)
 
794
 
 
795
    def mark(self, irc, msg, args, channel, target, kickmsg):
 
796
        """[<channel>] <nick|hostmask> [<comment>]
 
797
 
 
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
 
800
        """
 
801
        user = self.check_auth(irc, msg, args)
 
802
        if not user:
 
803
            return
 
804
 
 
805
        if target == '*' or target[0] == '*':
 
806
            irc.error("Can not create a mark for '%s'" % target)
 
807
            return
 
808
 
 
809
        if not channel:
 
810
            irc.error('<channel> must be given if not in a channel')
 
811
            return
 
812
        channel = channel.lower()
 
813
        channels = []
 
814
        for chan in irc.state.channels.keys():
 
815
            channels.append(chan.lower())
 
816
 
 
817
        if not channel in channels:
 
818
            irc.error('Not in that channel')
 
819
            return
 
820
 
 
821
        if not kickmsg:
 
822
            kickmsg = '**MARK**'
 
823
        else:
 
824
            kickmsg = "**MARK** - %s" % kickmsg
 
825
        hostmask = self.nick_to_host(irc, target)
 
826
 
 
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)
 
829
        irc.replySuccess()
 
830
 
 
831
    mark = wrap(mark, [optional('channel'), 'something', additional('text')])
 
832
 
 
833
    def sort_bans(self, channel=None):
 
834
        data = self.db_run("SELECT mask, removal, channel, id FROM bans", (), expect_result=True)
 
835
        if channel:
 
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]]
 
839
        return mutes + bans
 
840
 
 
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)
 
843
        if data:
 
844
            data = data[0]
 
845
        if not data or not data[0]:
 
846
            return
 
847
        return int(data[0])
 
848
 
 
849
    def getBans(self, hostmask, channel):
 
850
        match = []
 
851
        if 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)
 
857
                for e in data:
 
858
                    if hostmaskPatternEqual(e[0], hostmask):
 
859
                        if (e[0], e[1]) not in match:
 
860
                            match.append((e[0], e[1]))
 
861
        else:
 
862
            for c in self.bans:
 
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()
 
867
            for e in data:
 
868
                    if hostmaskPatternEqual(e[0], hostmask):
 
869
                        if (e[0], e[1]) not in match:
 
870
                            match.append((e[0], e[1]))
 
871
        return match
 
872
 
 
873
    def bansearch_real(self, irc, msg, args, target, channel, from_reply=False, reply=None):
 
874
        """<nick|hostmask> [<channel>]
 
875
 
 
876
        Search bans database for a ban on <nick|hostmask>,
 
877
        if <channel> is not given search all channel bans.
 
878
        """
 
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")
 
882
            ret.append(t)
 
883
            return tuple(ret)
 
884
 
 
885
        user = self.check_auth(irc, msg, args)
 
886
        if not user:
 
887
            return
 
888
 
 
889
        if from_reply:
 
890
            if not reply:
 
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"))
 
895
            hostmask = reply
 
896
        else:
 
897
            hostmask = self.nick_to_host(irc, target, reply_now=False)
 
898
        if not hostmask:
 
899
            self.sendWhois(irc, target, True, 'bansearch', irc, msg, args, target, channel)
 
900
            return
 
901
        match = self.getBans(hostmask, channel)
 
902
 
 
903
        if capab(user, 'owner'):
 
904
            if len(queue.msgcache) > 0:
 
905
                irc.reply("Warning: still syncing (%i)" % len(queue.msgcache))
 
906
 
 
907
        if channel:
 
908
            if not ircutils.isChannel(channel):
 
909
                channel = None
 
910
 
 
911
        if '*' in target or '?' in target:
 
912
            irc.error("Can only search for a complete hostmask")
 
913
            return
 
914
        hostmask = target
 
915
        if '!' not in target or '@' not in target:
 
916
            hostmask = self.nick_to_host(irc, target)
 
917
        if '!' not in hostmask:
 
918
            if "n=" 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)
 
923
 
 
924
        if not match:
 
925
            irc.reply("No matches found for %s in %s" % (hostmask, True and channel or "any channel"))
 
926
            return
 
927
        ret = []
 
928
        replies = []
 
929
        for m in match:
 
930
            if m[1]:
 
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]))
 
932
        if not ret:
 
933
            done = []
 
934
            for c in self.bans:
 
935
                for b in self.bans[c]:
 
936
                    for m in match:
 
937
                        if m[0] == b.mask:
 
938
                            if not c in done:
 
939
                                irc.reply("Match %s in %s" % (b, c))
 
940
                                done.append(c)
 
941
            return
 
942
        for i in ret:
 
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],))
 
945
            else:
 
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))
 
949
 
 
950
        if replies:
 
951
            for r in replies:
 
952
                irc.reply(r[0], private=r[1])
 
953
            return
 
954
        irc.error("Something not so good happened, please tell stdin about it")
 
955
 
 
956
    bansearch = wrap(bansearch_real, ['something', optional('something', default=None)])
 
957
 
 
958
    def banlog(self, irc, msg, args, target, channel):
 
959
        """<nick|hostmask> [<channel>]
 
960
 
 
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.
 
964
        """
 
965
        user = self.check_auth(irc, msg, args)
 
966
        if not user:
 
967
            return
 
968
 
 
969
        if capab(user, 'owner') and len(queue.msgcache) > 0:
 
970
            irc.reply("Warning: still syncing (%i)" % len(queue.msgcache))
 
971
 
 
972
        hostmask = self.nick_to_host(irc, target)
 
973
        target = target.split('!', 1)[0]
 
974
        match = self.getBans(hostmask, channel)
 
975
 
 
976
        if not match:
 
977
            irc.reply("No matches found for %s (%s) in %s" % (target, hostmask, True and channel or "any channel"))
 
978
            return
 
979
 
 
980
        ret = []
 
981
        for m in match:
 
982
            if m[1]:
 
983
                ret.append((self.db_run("SELECT log, channel FROM bans WHERE id=%d", m[1], expect_result=True), m[1]))
 
984
 
 
985
        sent = []
 
986
        if not ret:
 
987
            irc.reply("No matches in tracker")
 
988
        for logs in ret:
 
989
            log = logs[0]
 
990
            id = logs[1]
 
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()]
 
992
            show_sep = False
 
993
            if not lines:
 
994
                show_sep = False
 
995
                irc.error("No log for ID %s available" % id)
 
996
            else:
 
997
                for l in lines[:5]:
 
998
                    if l not in sent:
 
999
                        show_sep = True
 
1000
                        irc.reply(l)
 
1001
                        sent.append(l)
 
1002
            if show_sep:
 
1003
                irc.reply('--')
 
1004
 
 
1005
    banlog = wrap(banlog, ['something', optional('anything', default=None)])
 
1006
 
 
1007
    def updatebt(self, irc, msg, args, channel):
 
1008
        """[<channel>]
 
1009
 
 
1010
        Update bans in the tracker from the channel ban list,
 
1011
        if channel is not given then run in all channels
 
1012
        """
 
1013
 
 
1014
        def getBans(chan):
 
1015
            data = self.db_run("SELECT mask, removal FROM bans WHERE channel=%s", chan, expect_result=True)
 
1016
            L = []
 
1017
            for mask, removal in data:
 
1018
                if removal is not None:
 
1019
                    continue
 
1020
                elif not isUserHostmask(mask) and mask[0] != '$':
 
1021
                    continue
 
1022
                L.append(mask)
 
1023
            return L
 
1024
 
 
1025
        def remBans(chan):
 
1026
            bans = getBans(chan)
 
1027
            old_bans = bans[:]
 
1028
            new_bans = [i.mask for i in self.bans[chan]]
 
1029
            remove_bans = []
 
1030
            for ban in old_bans:
 
1031
                if ban not in new_bans:
 
1032
                    remove_bans.append(ban)
 
1033
                    bans.remove(ban)
 
1034
 
 
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)
 
1038
 
 
1039
            return len(remove_bans)
 
1040
 
 
1041
        def addBans(chan):
 
1042
            bans = self.bans[chan]
 
1043
            old_bans = getBans(chan)
 
1044
            add_bans = []
 
1045
            for ban in bans:
 
1046
                if ban.mask not in old_bans and ban not in add_bans:
 
1047
                    add_bans.append(ban)
 
1048
 
 
1049
            for ban in add_bans:
 
1050
                nick = ban.who
 
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)
 
1057
 
 
1058
        if not self.check_auth(irc, msg, args, 'owner'):
 
1059
            return
 
1060
 
 
1061
        add_res = 0
 
1062
        rem_res = 0
 
1063
 
 
1064
        if len(queue.msgcache) > 0:
 
1065
            irc.reply("Error: still syncing (%i)" % len(queue.msgcache))
 
1066
            return
 
1067
 
 
1068
        try:
 
1069
            if channel:
 
1070
                rem_res += remBans(channel)
 
1071
                add_res += addBans(channel)
 
1072
            else:
 
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)
 
1078
        except KeyError, e:
 
1079
            irc.error("%s, Please wait longer" % e)
 
1080
            return
 
1081
 
 
1082
        irc.reply("Cleared %i obsolete bans, Added %i new bans" % (rem_res, add_res))
 
1083
 
 
1084
    updatebt = wrap(updatebt, [optional('anything', default=None)])
 
1085
 
 
1086
    def comment(self, irc, msg, args, id, kickmsg):
 
1087
        """<id> [<comment>]
 
1088
 
 
1089
        Reads or adds the <comment> for the ban with <id>,
 
1090
        use @bansearch to find the id of a ban
 
1091
        """
 
1092
        def addComment(id, nick, msg):
 
1093
            n = now()
 
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)
 
1097
 
 
1098
        nick = msg.nick
 
1099
        if kickmsg:
 
1100
            addComment(id, nick, kickmsg)
 
1101
            irc.replySuccess()
 
1102
        else:
 
1103
            data = readComment(id)
 
1104
            if data:
 
1105
                for c in data:
 
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()) )
 
1107
            else:
 
1108
                irc.error("No comments recorded for ban %i" % id)
 
1109
    comment = wrap(comment, ['id', optional('text')])
 
1110
 
 
1111
    def banlink(self, irc, msg, args, id, highlight):
 
1112
        """<id> [<highlight>]
 
1113
 
 
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
 
1116
        """
 
1117
        if not self.check_auth(irc, msg, args):
 
1118
            return
 
1119
        if not highlight:
 
1120
            irc.reply("%s/bans.cgi?log=%s" % (self.registryValue('bansite'), id), private=True)
 
1121
        else:
 
1122
            irc.reply("%s/bans.cgi?log=%s&mark=%s" % (self.registryValue('bansite'), id, highlight), private=True)
 
1123
    banlink = wrap(banlink, ['id', optional('somethingWithoutSpaces')])
 
1124
 
 
1125
    def banreview(self, irc, msg, args):
 
1126
        """
 
1127
        Lists pending ban reviews."""
 
1128
        if not self.check_auth(irc, msg, args):
 
1129
            return
 
1130
        count = {}
 
1131
        for reviews in self.pendingReviews.itervalues():
 
1132
            for nick, msg in reviews:
 
1133
                try:
 
1134
                    count[nick] += 1
 
1135
                except KeyError:
 
1136
                    count[nick] = 1
 
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)
 
1140
        irc.reply(s)
 
1141
 
 
1142
    banreview = wrap(banreview)
 
1143
 
 
1144
Class = Bantracker