2010-03-10 07:27:00 +01:00
###
# Copyright (c) 2010, Daniel Folkinshteyn
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot . utils as utils
from supybot . commands import *
import supybot . plugins as plugins
import supybot . ircutils as ircutils
import supybot . callbacks as callbacks
2010-03-11 07:59:37 +01:00
import supybot . conf as conf
import supybot . ircdb as ircdb
2010-03-10 07:27:00 +01:00
import re
2010-03-11 07:59:37 +01:00
import os
import time
2010-03-16 01:06:24 +01:00
#try:
#import sqlite
#except ImportError:
#raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
#'plugin. Download it at ' \
#'<http://code.google.com/p/pysqlite/>'
2010-03-10 07:27:00 +01:00
2010-03-17 06:55:23 +01:00
try :
import sqlite3
except :
from pysqlite2 import dbapi2 as sqlite3 # for python2.4
2010-03-16 01:06:24 +01:00
# these are needed cuz we are overriding getdb
import threading
import supybot . world as world
2010-03-10 07:27:00 +01:00
2010-03-16 21:49:55 +01:00
import supybot . log as log
2010-03-11 07:59:37 +01:00
class MessageParser ( callbacks . Plugin , plugins . ChannelDBHandler ) :
2010-03-10 07:27:00 +01:00
""" This plugin can set regexp triggers to activate the bot.
Use ' add ' command to add regexp trigger , ' remove ' to remove . """
threaded = True
def __init__ ( self , irc ) :
2010-03-11 07:59:37 +01:00
callbacks . Plugin . __init__ ( self , irc )
plugins . ChannelDBHandler . __init__ ( self )
def makeDb ( self , filename ) :
2010-03-16 01:06:24 +01:00
""" Create the database and connect to it. """
2010-03-11 07:59:37 +01:00
if os . path . exists ( filename ) :
2010-03-17 18:19:07 +01:00
db = sqlite3 . connect ( filename )
db . text_factory = str
return db
2010-03-16 01:06:24 +01:00
db = sqlite3 . connect ( filename )
2010-03-17 18:19:07 +01:00
db . text_factory = str
2010-03-11 07:59:37 +01:00
cursor = db . cursor ( )
cursor . execute ( """ CREATE TABLE triggers (
id INTEGER PRIMARY KEY ,
regexp TEXT UNIQUE ON CONFLICT REPLACE ,
added_by TEXT ,
added_at TIMESTAMP ,
usage_count INTEGER ,
action TEXT ,
locked BOOLEAN
) """ )
db . commit ( )
return db
2010-03-16 01:06:24 +01:00
# override this because sqlite3 doesn't have autocommit
# use isolation_level instead.
def getDb ( self , channel ) :
""" Use this to get a database for a specific channel. """
currentThread = threading . currentThread ( )
if channel not in self . dbCache and currentThread == world . mainThread :
self . dbCache [ channel ] = self . makeDb ( self . makeFilename ( channel ) )
if currentThread != world . mainThread :
db = self . makeDb ( self . makeFilename ( channel ) )
else :
db = self . dbCache [ channel ]
db . isolation_level = None
return db
2010-03-11 07:59:37 +01:00
def _updateRank ( self , channel , regexp ) :
if self . registryValue ( ' keepRankInfo ' , channel ) :
db = self . getDb ( channel )
cursor = db . cursor ( )
cursor . execute ( """ SELECT usage_count
FROM triggers
2010-03-16 01:06:24 +01:00
WHERE regexp = ? """ , (regexp,))
2010-03-11 07:59:37 +01:00
old_count = cursor . fetchall ( ) [ 0 ] [ 0 ]
2010-03-16 01:06:24 +01:00
cursor . execute ( " UPDATE triggers SET usage_count=? WHERE regexp=? " , ( old_count + 1 , regexp , ) )
2010-03-11 07:59:37 +01:00
db . commit ( )
2010-03-10 07:27:00 +01:00
2010-03-16 21:49:55 +01:00
def _runCommandFunction ( self , irc , msg , command ) :
""" Run a command from message, as if command was sent over IRC. """
# need to encode it from unicode, since sqlite stores text as unicode.
2010-03-17 18:19:07 +01:00
tokens = callbacks . tokenize ( command )
2010-03-16 21:49:55 +01:00
try :
self . Proxy ( irc . irc , msg , tokens )
except Exception , e :
log . exception ( ' Uncaught exception in scheduled function: ' )
2010-03-11 07:59:37 +01:00
def doPrivmsg ( self , irc , msg ) :
2010-03-10 07:27:00 +01:00
channel = msg . args [ 0 ]
if not irc . isChannel ( channel ) :
return
if self . registryValue ( ' enable ' , channel ) :
2010-03-18 04:54:28 +01:00
if callbacks . addressed ( irc . nick , msg ) : #message is direct command
return
2010-03-11 07:59:37 +01:00
actions = [ ]
db = self . getDb ( channel )
cursor = db . cursor ( )
cursor . execute ( " SELECT regexp, action FROM triggers " )
2010-03-16 01:06:24 +01:00
results = cursor . fetchall ( )
if len ( results ) == 0 :
2010-03-11 07:59:37 +01:00
return
2010-03-16 01:06:24 +01:00
for ( regexp , action ) in results :
2010-03-17 17:37:34 +01:00
for match in re . finditer ( regexp , msg . args [ 1 ] ) :
if match is not None :
thisaction = action
self . _updateRank ( channel , regexp )
for ( i , j ) in enumerate ( match . groups ( ) ) :
thisaction = re . sub ( r ' \ $ ' + str ( i + 1 ) , match . group ( i + 1 ) , thisaction )
actions . append ( thisaction )
2010-03-11 07:59:37 +01:00
2010-03-16 21:49:55 +01:00
for action in actions :
self . _runCommandFunction ( irc , msg , action )
2010-03-11 07:59:37 +01:00
def add ( self , irc , msg , args , channel , regexp , action ) :
""" [<channel>] <regexp> <action>
Associates < regexp > with < action > . < channel > is only
necessary if the message isn ' t sent on the channel
itself . Action is echoed upon regexp match , with variables $ 1 , $ 2 ,
etc . being interpolated from the regexp match groups . """
db = self . getDb ( channel )
cursor = db . cursor ( )
2010-03-19 14:55:43 +01:00
cursor . execute ( " SELECT id, usage_count, locked FROM triggers WHERE regexp=? " , ( regexp , ) )
2010-03-16 01:06:24 +01:00
results = cursor . fetchall ( )
if len ( results ) != 0 :
2010-03-19 14:55:43 +01:00
( id , usage_count , locked ) = map ( int , results [ 0 ] )
2010-03-11 07:59:37 +01:00
else :
2010-03-19 14:55:43 +01:00
locked = 0
usage_count = 0
2010-03-11 07:59:37 +01:00
if not locked :
2010-03-19 18:20:50 +01:00
try :
re . compile ( regexp )
except Exception , e :
irc . error ( ' Invalid python regexp: %s ' % ( e , ) )
return
2010-03-11 07:59:37 +01:00
if ircdb . users . hasUser ( msg . prefix ) :
name = ircdb . users . getUser ( msg . prefix ) . name
else :
name = msg . nick
cursor . execute ( """ INSERT INTO triggers VALUES
2010-03-16 01:06:24 +01:00
( NULL , ? , ? , ? , ? , ? , ? ) """ ,
2010-03-19 14:55:43 +01:00
( regexp , name , int ( time . time ( ) ) , usage_count , action , locked , ) )
2010-03-11 07:59:37 +01:00
db . commit ( )
irc . replySuccess ( )
else :
2010-03-15 22:32:02 +01:00
irc . reply ( ' That trigger is locked. ' )
return
2010-03-11 07:59:37 +01:00
add = wrap ( add , [ ' channel ' , ' something ' , ' something ' ] )
2010-03-19 18:40:36 +01:00
def remove ( self , irc , msg , args , channel , optlist , regexp ) :
""" [<channel>] [--id] <regexp>]
2010-03-11 07:59:37 +01:00
Removes the trigger for < regexp > from the triggers database .
< channel > is only necessary if
the message isn ' t sent in the channel itself.
2010-03-19 18:40:36 +01:00
If option - - id specified , will retrieve by regexp id , not content .
2010-03-11 07:59:37 +01:00
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
2010-03-19 18:40:36 +01:00
target = ' regexp '
for ( option , arg ) in optlist :
if option == ' id ' :
target = ' id '
sql = " SELECT id, locked FROM triggers WHERE %s =? " % ( target , )
cursor . execute ( sql , ( regexp , ) )
2010-03-16 01:06:24 +01:00
results = cursor . fetchall ( )
if len ( results ) != 0 :
( id , locked ) = map ( int , results [ 0 ] )
2010-03-11 07:59:37 +01:00
else :
2010-03-15 22:32:02 +01:00
irc . reply ( ' There is no such regexp trigger. ' )
return
2010-03-11 07:59:37 +01:00
if locked :
2010-03-15 22:32:02 +01:00
irc . reply ( ' This regexp trigger is locked. ' )
return
2010-03-11 07:59:37 +01:00
2010-03-16 01:06:24 +01:00
cursor . execute ( """ DELETE FROM triggers WHERE id=? """ , ( id , ) )
2010-03-11 07:59:37 +01:00
db . commit ( )
irc . replySuccess ( )
2010-03-19 18:40:36 +01:00
remove = wrap ( remove , [ ' channel ' ,
getopts ( { ' id ' : ' ' , } ) ,
' something ' ] )
2010-03-11 07:59:37 +01:00
2010-03-19 15:44:23 +01:00
def lock ( self , irc , msg , args , channel , regexp ) :
""" [<channel>] <regexp>
Locks the < regexp > so that it cannot be
removed or overwritten to . < channel > is only necessary if the message isn ' t
sent in the channel itself .
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
cursor . execute ( " SELECT id FROM triggers WHERE regexp=? " , ( regexp , ) )
results = cursor . fetchall ( )
if len ( results ) == 0 :
irc . reply ( ' There is no such regexp trigger. ' )
return
cursor . execute ( " UPDATE triggers SET locked=1 WHERE regexp=? " , ( regexp , ) )
db . commit ( )
irc . replySuccess ( )
lock = wrap ( lock , [ ' channel ' , ' text ' ] )
def unlock ( self , irc , msg , args , channel , regexp ) :
""" [<channel>] <regexp>
Unlocks the entry associated with < regexp > so that it can be
removed or overwritten . < channel > is only necessary if the message isn ' t
sent in the channel itself .
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
cursor . execute ( " SELECT id FROM triggers WHERE regexp=? " , ( regexp , ) )
results = cursor . fetchall ( )
if len ( results ) == 0 :
irc . reply ( ' There is no such regexp trigger. ' )
return
cursor . execute ( " UPDATE triggers SET locked=0 WHERE regexp=? " , ( regexp , ) )
db . commit ( )
irc . replySuccess ( )
unlock = wrap ( unlock , [ ' channel ' , ' text ' ] )
2010-03-19 18:34:50 +01:00
def show ( self , irc , msg , args , channel , optlist , regexp ) :
""" [<channel>] [--id] <regexp>
2010-03-11 07:59:37 +01:00
Looks up the value of < regexp > in the triggers database .
< channel > is only necessary if the message isn ' t sent in the channel
itself .
2010-03-19 18:34:50 +01:00
If option - - id specified , will retrieve by regexp id , not content .
2010-03-11 07:59:37 +01:00
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
2010-03-19 18:34:50 +01:00
target = ' regexp '
for ( option , arg ) in optlist :
if option == ' id ' :
target = ' id '
sql = " SELECT regexp, action FROM triggers WHERE %s =? " % ( target , )
cursor . execute ( sql , ( regexp , ) )
2010-03-16 01:06:24 +01:00
results = cursor . fetchall ( )
if len ( results ) != 0 :
( regexp , action ) = results [ 0 ]
2010-03-11 07:59:37 +01:00
else :
2010-03-15 22:32:02 +01:00
irc . reply ( ' There is no such regexp trigger. ' )
return
2010-03-11 07:59:37 +01:00
2010-03-19 18:34:50 +01:00
irc . reply ( " The action for regexp trigger \" %s \" is \" %s \" " % ( regexp , action ) )
show = wrap ( show , [ ' channel ' ,
getopts ( { ' id ' : ' ' , } ) ,
' something ' ] )
2010-03-11 07:59:37 +01:00
2010-03-19 20:34:35 +01:00
def info ( self , irc , msg , args , channel , optlist , regexp ) :
""" [<channel>] [--id] <regexp>
Display information about < regexp > in the triggers database .
< channel > is only necessary if the message isn ' t sent in the channel
itself .
If option - - id specified , will retrieve by regexp id , not content .
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
target = ' regexp '
for ( option , arg ) in optlist :
if option == ' id ' :
target = ' id '
sql = " SELECT * FROM triggers WHERE %s =? " % ( target , )
cursor . execute ( sql , ( regexp , ) )
results = cursor . fetchall ( )
if len ( results ) != 0 :
( id , regexp , added_by , added_at , usage_count , action , locked ) = results [ 0 ]
else :
irc . reply ( ' There is no such regexp trigger. ' )
return
irc . reply ( " The regexp trigger id is %d , regexp is \" %s \" , and action is \" %s \" . It was added by user %s on %s , has been triggered %d times, and is %s . " % ( id ,
regexp ,
action ,
added_by ,
time . strftime ( conf . supybot . reply . format . time ( ) ,
time . localtime ( int ( added_at ) ) ) ,
usage_count ,
locked and " locked " or " not locked " , ) )
info = wrap ( info , [ ' channel ' ,
getopts ( { ' id ' : ' ' , } ) ,
' something ' ] )
2010-03-11 07:59:37 +01:00
def listall ( self , irc , msg , args , channel ) :
""" [<channel>]
Lists regexps present in the triggers database .
< channel > is only necessary if the message isn ' t sent in the channel
2010-03-19 18:24:45 +01:00
itself . Regexp ID listed in paretheses .
2010-03-11 07:59:37 +01:00
"""
db = self . getDb ( channel )
cursor = db . cursor ( )
2010-03-19 18:24:45 +01:00
cursor . execute ( " SELECT regexp, id FROM triggers " )
2010-03-16 01:06:24 +01:00
results = cursor . fetchall ( )
if len ( results ) != 0 :
regexps = results
2010-03-11 07:59:37 +01:00
else :
2010-03-15 22:32:02 +01:00
irc . reply ( ' There are no regexp triggers in the database. ' )
return
2010-03-11 07:59:37 +01:00
2010-03-19 20:54:54 +01:00
s = [ " \" %s \" ( %d ) " % ( regexp [ 0 ] , regexp [ 1 ] ) for regexp in regexps ]
irc . reply ( ' , ' . join ( s ) )
2010-03-11 07:59:37 +01:00
listall = wrap ( listall , [ ' channel ' ] )
def triggerrank ( self , irc , msg , args , channel ) :
""" [<channel>]
Returns a list of top - ranked regexps , sorted by usage count
( rank ) . The number of regexps returned is set by the
rankListLength registry value . < channel > is only necessary if the
message isn ' t sent in the channel itself.
"""
numregexps = self . registryValue ( ' rankListLength ' , channel )
db = self . getDb ( channel )
cursor = db . cursor ( )
cursor . execute ( """ SELECT regexp, usage_count
FROM triggers
ORDER BY usage_count DESC
2010-03-16 01:06:24 +01:00
LIMIT ? """ , (numregexps,))
2010-03-11 07:59:37 +01:00
regexps = cursor . fetchall ( )
s = [ " # %d %s ( %d ) " % ( i + 1 , regexp [ 0 ] , regexp [ 1 ] ) for i , regexp in enumerate ( regexps ) ]
irc . reply ( " , " . join ( s ) )
triggerrank = wrap ( triggerrank , [ ' channel ' ] )
2010-03-10 07:27:00 +01:00
Class = MessageParser
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: