<commit_before>#!/usr/bin/env python
# coding=utf-8
"""
bucket.py - Jenni module to emulate the behavior of #xkcd's Bucket bot
Copyright 2012, <NAME>, http://embolalia.net
Copyright © 2012, <NAME> <<EMAIL>>
Licensed under the Eiffel Forum License 2.

https://github.com/embolalia/jenni

This module is built without using code from the original bucket, but using the same DB table format for factoids.

Things to know if you extend this module:

All inventory items are managed by the inventory class.
All runtime information is in the runtime information class

To prevent Jenni from outputting a "Don't Know" message when referred use the following line:

bucket_runtime_data.inhibit_reply = trigger.group(0)

and make sure the priority of your callable is medium or higher.
"""
import MySQLdb, re
from re import sub
from random import randint, seed
import web
import os
from collections import deque
from tools import Ddict
seed()

def configure(config):
    chunk = ''
    if config.option('Configure Bucket factiod DB', True):
        config.interactive_add('bucket_host', "Enter the MySQL hostname", 'localhost')
        config.interactive_add('bucket_user', "Enter the MySQL username")
        config.interactive_add('bucket_pass', "Enter the user's password")
        config.interactive_add('bucket_db', "Enter the name of the database to use")
        config.interactive_add('bucket_literal_path', "Enter the path in which you want to store output of the literal command")
        config.interactive_add('bucket_literal_baseurl', "Base URL for literal output")
        chunk = ("\nbucket_host = '%s'\nbucket_user = '%s'\nbucket_pass = '%s'\nbucket_db = '%s'\nbucket_literal_path = '%s'\nbucket_literal_baseurl = '%s'\n"
                 % (config.bucket_host, config.bucket_user, config.bucket_pass, config.bucket_db, config.bucket_literal_path, config.bucket_literal_baseurl))
        if config.option('do you want to generate bucket tables and populate them with some default data?', True):
            db = MySQLdb.connect(host=config.bucket_host,
                         user=config.bucket_user,
                         passwd=config.bucket_pass,
                         db=config.bucket_db)
            cur = db.cursor()
            #Create facts table
            cur.execute("CREATE TABLE IF NOT EXISTS `bucket_facts` (`id` int(10) unsigned NOT NULL auto_increment, `fact` varchar(128) NOT NULL,`tidbit` text NOT NULL,`verb` varchar(16) NOT NULL default 'is',`RE` tinyint(1) NOT NULL,`protected` tinyint(1) NOT NULL,`mood` tinyint(3) unsigned default NULL,`chance` tinyint(3) unsigned default NULL,PRIMARY KEY (`id`),UNIQUE KEY `fact` (`fact`,`tidbit`(200),`verb`),KEY `trigger` (`fact`),KEY `RE` (`RE`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;")
            #Create inventory table
            cur.execute("CREATE TABLE IF NOT EXISTS `bucket_items` (`id` int(10) unsigned NOT NULL auto_increment,`channel` varchar(64) NOT NULL,`what` varchar(255) NOT NULL,`user` varchar(64) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `what` (`what`),KEY `from` (`user`),KEY `where` (`channel`)) ENGINE=MyISAM DEFAULT CHARSET=latin1 ;")
            #Insert a Don't Know factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('Don\'t Know', '++?????++ Out of Cheese Error. Redo From Start.', '<reply>', False, False, None, None))
            #Insert a pickup full factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('pickup full', 'takes $item but drops $giveitem', '<action>', False, False, None, None))
            #Insert a duplicate item factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('duplicate item', 'No thanks, I\'ve already got $item', '<reply>', False, False, None, None))
            #Insert a take item factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('takes item', 'Oh, thanks, I\'ll keep this $item safe', '<reply>', False, False, None, None))
            db.commit()
            db.close()
    return chunk
    
class Inventory():
    ''' Everything inventory related '''
    avilable_items = []
    current_items = deque([]) #FIFO. Max length 15
    def add_random(self):
        ''' Adds a random item to the inventory'''
        item = self.avilable_items[randint(0, len(self.avilable_items)-1)].strip()
        if item in self.current_items:
            try:
                return self.add_random()
            except RuntimeError:
                #Too much recursion, this can only mean all avilable_items are in current_items. Bananas.
                self.current_items.appendleft('bananas!')
                return 'bananas!'
        self.current_items.appendleft(item)
        return item
    def add(self, item, user, channel, jenni):
        ''' Adds an item to the inventory'''
        dropped = False
        item = item.strip()
        if item.lower() not in [x.lower() for x in self.avilable_items]:
            db = connect_db(jenni)
            cur = db.cursor()
            try:
                cur.execute('INSERT INTO bucket_items (`channel`, `what`, `user`) VALUES (%s, %s, %s);', (channel, item.encode('utf8'), user))
            except MySQLdb.IntegrityError, e:
                jenni.debug('bucket', 'IntegrityError in inventory code', 'warning')
                jenni.debug('bucket', str(e), 'warning')
            db.commit()
            db.close()
            self.avilable_items.append(item)
        if item in self.current_items:
            return '%ERROR% duplicate item %ERROR%'
        if len(self.current_items) >= 15:
            dropped = True
        self.current_items.appendleft(item)
        return dropped
    def random_item(self):
        ''' returns a random item '''
        if len(self.current_items) == 0:
            return 'bananas!'
        item = self.current_items[randint(0, len(self.current_items)-1)]
        return item
    def give_item(self):
        ''' returns a random item and removes it from the inventory '''
        item = self.random_item()
        try:
            self.current_items.remove(item)
        except ValueError:
            pass
        return item
    def remove(self, item):
        ''' Attemt to remove an item from the inventory, returns False if failed '''
        try:
            self.current_items.remove(item)
            return True
        except ValueError:
            return False

class bucket_runtime_data():
    dont_know_cache = [] #Caching all the Don't Know factoids to reduce amount of DB reads
    what_was_that = {} #Remembering info of last DB read, per channel. for use with the "what was that" command.
    inhibit_reply = '' #Used to inhibit reply of an error message after teaching a factoid
    last_teach = {}
    last_lines = Ddict(dict) #For quotes.
    inventory = None
    shut_up = False
    special_verbs = ['<reply>', '<directreply>', '<directaction>', '<action>', '<alias>']
    inv_steal_match = None #Will contain a compiled regex object to check for inventory 'steal' event
    inv_give_match = None #Will contain a compiled regex object to check for inventory 'give' event

def remove_punctuation(string):
    return sub("[,\.\!\?\;\:]", '', string)
def setup(jenni):
    print 'Setting up Bucket...'
    db = None
    cur = None
    try:
        db = connect_db(jenni)
    except:
        print 'Error connecting to the bucket database.'
        return
    bucket_runtime_data.inventory = Inventory()
    cur = db.cursor()
    #caching "Don't Know" replies
    cur.execute('SELECT * FROM bucket_facts WHERE fact = "Don\'t Know";')
    results = cur.fetchall()
    cur.execute('SELECT * FROM bucket_items;')
    items = cur.fetchall()
    db.close()
    for result in results:
        bucket_runtime_data.dont_know_cache.append(result[2])
    for item in items:
        bucket_runtime_data.inventory.avilable_items.append(item[2])
    bucket_runtime_data.inv_give_match = re.compile('((^\001ACTION (gives|hands) %s)|^%s. take this) (.*)' % (jenni.nick, jenni.nick), re.I)
    bucket_runtime_data.inv_steal_match = re.compile('^\001ACTION (steals|takes) %s\'s (.*)' % jenni.nick, re.I)
    print 'Done setting up Bucket!'
def add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance):
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', (fact, tidbit, verb, re, protected, mood, chance))
        db.commit()
    except MySQLdb.IntegrityError:
        jenni.say("I already had it that way!")
        return False
    finally:
        db.close()
    bucket_runtime_data.last_teach[trigger.sender] = [fact,verb,tidbit]
    jenni.say("Okay, "+trigger.nick)

def teach_is_are(jenni, trigger):
    """Teaches a is b and a are b"""
    fact = trigger.group(1)
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    fact = remove_punctuation(fact)
    tidbit = trigger.group(3)
    verb = trigger.group(2)
    re = False
    protected = False
    mood = None
    chance = None
    for word in trigger.group(0).lower().split(' '):
        if word in bucket_runtime_data.special_verbs:
            return #do NOT teach is are if the trigger is similar to "Lemon, Who is the king of the derps? <reply> I am!" (contains both is|are and a special verb.
    
    add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
teach_is_are.rule = ('$nick', '(.*?) (is|are) (.*)')
teach_is_are.priority = 'high'

def teach_verb(jenni, trigger):
    """Teaches verbs/ambiguous reply"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    fact = trigger.group(1)
    fact = remove_punctuation(fact)
    tidbit = trigger.group(3)
    verb = trigger.group(2)
    re = False
    protected = False
    mood = None
    chance = None
    
    if verb not in bucket_runtime_data.special_verbs:
        verb = verb[1:-1]
    
    if fact == tidbit and verb == '<alias>':
        jenni.reply('You can\'t alias like this!')
        return
    add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
    if fact.lower() == 'don\'t know':
        bucket_runtime_data.dont_know_cache.append(tidbit)
teach_verb.rule = ('$nick', '(.*?) (<\S+>) (.*)')
teach_verb.priority = 'high'

def save_quote(jenni, trigger):
    """Saves a quote"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    quotee = trigger.group(1).lower()
    word = trigger.group(2)
    fact = quotee+' quotes'
    verb = '<reply>'
    re = False
    protected = False
    mood = None
    chance = None
    try:
        memory = bucket_runtime_data.last_lines[trigger.sender][quotee]
    except KeyError:
        jenni.say("Sorry, I don't remember what %s said about %s" % (quotee, word))
        return
    for line in memory:
        if remove_punctuation(word.lower()) in remove_punctuation(line.lower()):
            if line.startswith('\001ACTION'):
                line = line[len('\001ACTION '):-1]
                tidbit = '* %s %s' % (quotee, line)
            else:
                tidbit = '<%s> %s' % (quotee, line)
            add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
            jenni.say("Remembered that %s <reply> %s" % (fact, tidbit))
            return
    jenni.say("Sorry, I don't remember what %s said about %s" % (quotee, word))
save_quote.rule = ('$nick', 'remember (.*?) (.*)')
save_quote.priority = 'high'

def delete_factoid(jenni, trigger):
    """Delets a factoid"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    was = bucket_runtime_data.what_was_that
    if not trigger.admin:
        was[trigger.sender] = dont_know(jenni)
        return
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('SELECT * FROM bucket_facts WHERE ID = %s;', int(trigger.group(1)))
        results = cur.fetchall()
        if len(results)>1:
            jenni.debug('bucket', 'More than one factoid with the same ID?', 'warning')
            jenni.debug('bucket', str(results), 'warning')
            jenni.say('More than one factoid with the same ID. I refuse to continue.')
            return
        elif len(results) == 0:
            jenni.reply('No such factoid')
            return
        cur.execute('DELETE FROM bucket_facts WHERE ID = %s',int(trigger.group(1)))
        db.commit()
    except:
        jenni.say("Delete failed! are you sure this is a valid factoid ID?")
        return
    finally:
        db.close()
    line = results[0]
    fact, tidbit, verb = parse_factoid(line)
    jenni.say("Okay, %s, forgot that %s %s %s" % (trigger.nick, fact, verb, tidbit))
    
delete_factoid.rule = ('$nick', 'delete #(.*)')
delete_factoid.priority = 'high'

def undo_teach(jenni, trigger):
    """Undo teaching factoid"""
    was = bucket_runtime_data.what_was_that
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    if not trigger.admin:
        was[trigger.sender] = dont_know(jenni)
        return
    last_teach = bucket_runtime_data.last_teach
    fact = ''
    verb = ''
    tidbit = ''
    try:
        fact = last_teach[trigger.sender][0]
        verb = last_teach[trigger.sender][1]
        tidbit = last_teach[trigger.sender][2]
    except KeyError:
        jenni.reply('Nothing to undo!')
        return
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('DELETE FROM bucket_facts WHERE `fact` = %s AND `verb` = %s AND `tidbit` = %s', (fact, verb, tidbit))
        db.commit()
    except:
        jenni.say("Undo failed, this shouldn't have happened!")
        return
    finally:
        db.close()
    jenni.say("Okay, %s, forgot that %s %s %s" % (trigger.nick, fact, verb, tidbit))
    
undo_teach.rule = ('$nick', 'undo last')
undo_teach.priority = 'high'

def say_fact(jenni, trigger):
    """Response, if needed"""
    query = trigger.group(0)
    was = bucket_runtime_data.what_was_that
    inventory = bucket_runtime_data.inventory
    db = None
    cur = None
    results = None
    inv_give_match = bucket_runtime_data.inv_give_match.search(query)
    if inv_give_match is not None:
        item = inv_give_match.group(4)
        if item.endswith('\001'):
            item = item[:-1]
        item = item.strip()
        dropped = inventory.add(item, trigger.nick, trigger.sender, jenni)
        db = connect_db(jenni)
        cur = db.cursor()
        search_term = ''
        if dropped == False:
            #Query for 'takes item'
            search_term = 'takes item'
        elif dropped == '%ERROR% duplicate item %ERROR%':
            #Query for 'duplicate item'
            search_term = 'duplicate item'
        else:
            #Query for 'pickup full'
            search_term = 'pickup full'
        cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
        results = cur.fetchall()
        result = pick_result(results, jenni)
        fact, tidbit, verb = parse_factoid(result)
        tidbit = tidbit.replace('$item', item)
        tidbit = tidbit_vars(tidbit, trigger, False)

        if verb not in bucket_runtime_data.special_verbs:
            jenni.say("%s %s %s" % (fact, verb, tidbit))
        elif verb == '<reply>':
            jenni.say(tidbit)
        elif verb == '<action>':
            jenni.action(tidbit)
        was = result
        return
    inv_steal_match = bucket_runtime_data.inv_steal_match.search(query)
    if inv_steal_match is not None:
        item = inv_steal_match.group(2)
        if item.endswith('\001'):
            item = item[:-1]
        if (inventory.remove(item)):
            jenni.say('Hey! Give it back, it\'s mine!')
        else:
            jenni.say('But I don\'t have any %s' % item)
        return
    if query.startswith('\001ACTION'):
        query = query[len('\001ACTION '):]
    addressed = query.lower().startswith(jenni.nick.lower()) #Check if our nick was mentioned
    search_term = query.lower().strip() #What we are going to pass to MySql as our search term
    if addressed:
        search_term = search_term[(len(jenni.nick)+1):].strip() #Remove our nickname from the search term
    search_term = remove_punctuation(search_term).strip()

    if len(query) < 6 and not addressed:
        return #Ignore factoids shorter than 6 chars when not addressed
    if search_term == 'don\'t know' and not addressed:
        return #Ignore "don't know" when not addressed
    if not addressed and bucket_runtime_data.shut_up:
        return #Don't say anything if not addressed and shutting up
    if search_term == 'shut up' and addressed:
        jenni.reply('Okay...')
        bucket_runtime_data.shut_up = True
        return
    elif search_term == 'come back' and addressed:
        jenni.reply('I\'m back!')
        bucket_runtime_data.shut_up = False
        return
    literal = False
    inhibit = bucket_runtime_data.inhibit_reply
    if search_term.startswith('literal '):
        literal = True
        search_term = str.replace(str(search_term), 'literal ','')
    elif search_term == 'what was that' and addressed:
        try:
            jenni.say('That was '+ str(was[trigger.sender]))
        except KeyError:
            jenni.say('I have no idea')
        return
    elif search_term.startswith('reload') or search_term.startswith('update') or inhibit == search_term or inhibit == trigger.group(0):
        return #ignore commands such as reload or update, don't show 'Don't Know' responses for these 

    db = connect_db(jenni)
    cur = db.cursor()

    try:
        if search_term == 'random quote':
            cur.execute('SELECT * FROM bucket_facts WHERE fact LIKE "% quotes";')
            results = cur.fetchall()
        else:
            cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
            results = cur.fetchall()
    except UnicodeEncodeError, e:
        jenni.debug('bucket','Warning, database encoding error', 'warning')
        jenni.debug('bucket', e, 'warning')
    finally:
        db.close()
    if results == None:
        return
    result = pick_result(results, jenni)
    if addressed and result == None:
        was[trigger.sender] = 'Don\'t know: '+dont_know(jenni)
        return
    elif result == None:
        return
            
    fact, tidbit, verb = parse_factoid(result)
    tidbit = tidbit_vars(tidbit, trigger)

    if literal:
        if len(results) == 1:
            result = results[0]
            number = int(result[0])
            fact, tidbit, verb = parse_factoid(result)
            jenni.say ("#%d - %s %s %s" % (number, fact, verb, tidbit))
        else:
            jenni.reply('just a second, I\'ll make the list!')
            bucket_literal_path = jenni.config.bucket_literal_path
            bucket_literal_baseurl = jenni.config.bucket_literal_baseurl
            if not bucket_literal_baseurl.endswith('/'):
                bucket_literal_baseurl = bucket_literal_baseurl + '/'
            if not os.path.isdir(bucket_literal_path):
                try:
                    os.makedirs(bucket_literal_path)
                except Exception as e:
                    jenni.say("Can't create directory to store literal, sorry!")
                    jenni.say(e)
                    return
            f = open(os.path.join(bucket_literal_path, fact.lower()+'.txt'), 'w')
            for result in results:
                number = int(result[0])
                fact, tidbit, verb = parse_factoid(result)
                literal_line = "#%d - %s %s %s" % (number, fact, verb, tidbit)
                f.write(literal_line+'\n')
            f.close()
            jenni.reply('Here you go! %s (%d factoids)' % (bucket_literal_baseurl+web.quote(fact.lower()+'.txt'), len(results)))
        result = 'Me giving you a literal link'
    elif verb not in bucket_runtime_data.special_verbs:
        jenni.say("%s %s %s" % (fact, verb, tidbit))
    elif verb == '<reply>':
        jenni.say(tidbit)
    elif verb == '<action>':
        jenni.action(tidbit)
    elif verb == '<directreply>' and addressed:
        jenni.say(tidbit)
    elif verb == '<directaction>' and addressed:
        jenni.action(tidbit)
    was[trigger.sender] = result
    
    
say_fact.rule = ('(.*)')
say_fact.priority = 'low'

def pick_result(results, jenni):
    try:
        if len(results) == 1:
            result = results[0]
        elif len(results) > 1:
            result = results[randint(0, len(results)-1)]
        elif len(results) == 0:
            return None
        if result[3] == '<alias>':
            #Handle alias, recursive!
            db = connect_db(jenni)
            cur = db.cursor()
            search_term = result[2].strip()
            try:
                cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
                results = cur.fetchall()
            except UnicodeEncodeError, e:
                jenni.debug('bucket','Warning, database encoding error', 'warning')
                jenni.debug('bucket',e , 'warning')
            finally:
                db.close()
            result = pick_result(results, jenni)
        return result
    except RuntimeError, e:
        jenni.debug('bucket', 'RutimeError in pick_result', 'warning')
        jenni.debug('bucket', e, 'warning')
        return None

def get_inventory(jenni, trigger):
    ''' get a human readable list of the bucket inventory '''

    bucket_runtime_data.inhibit_reply = trigger.group(0)

    inventory = bucket_runtime_data.inventory

    readable_item_list = ''
    
    if len(inventory.current_items)==0:
        return jenni.action('is carrying nothing')
    for item in inventory.current_items:
        readable_item_list = readable_item_list + ' '+item.encode('utf8')+','

    jenni.action('is carrying'+readable_item_list)

get_inventory.rule = ('$nick','inventory')
get_inventory.priority = 'medium'

def connect_db(jenni):
    return MySQLdb.connect(host=jenni.config.bucket_host,
                         user=jenni.config.bucket_user,
                         passwd=jenni.config.bucket_pass,
                         db=jenni.config.bucket_db)
def tidbit_vars(tidbit, trigger, random_item=True):
    ''' Parse in-tidbit vars '''
    #Special in-tidbit vars:
    inventory = bucket_runtime_data.inventory<commit_msg>[bucket] Use unicode for the SQL connection<commit_after>#!/usr/bin/env python
# coding=utf-8
"""
bucket.py - Jenni module to emulate the behavior of #xkcd's Bucket bot
Copyright 2012, <NAME>, http://embolalia.net
Copyright © 2012, <NAME> <<EMAIL>>
Licensed under the Eiffel Forum License 2.

https://github.com/embolalia/jenni

This module is built without using code from the original bucket, but using the same DB table format for factoids.

Things to know if you extend this module:

All inventory items are managed by the inventory class.
All runtime information is in the runtime information class

To prevent Jenni from outputting a "Don't Know" message when referred use the following line:

bucket_runtime_data.inhibit_reply = trigger.group(0)

and make sure the priority of your callable is medium or higher.
"""
import MySQLdb, re
from re import sub
from random import randint, seed
import web
import os
from collections import deque
from tools import Ddict
seed()

def configure(config):
    chunk = ''
    if config.option('Configure Bucket factiod DB', True):
        config.interactive_add('bucket_host', "Enter the MySQL hostname", 'localhost')
        config.interactive_add('bucket_user', "Enter the MySQL username")
        config.interactive_add('bucket_pass', "Enter the user's password")
        config.interactive_add('bucket_db', "Enter the name of the database to use")
        config.interactive_add('bucket_literal_path', "Enter the path in which you want to store output of the literal command")
        config.interactive_add('bucket_literal_baseurl', "Base URL for literal output")
        chunk = ("\nbucket_host = '%s'\nbucket_user = '%s'\nbucket_pass = '%s'\nbucket_db = '%s'\nbucket_literal_path = '%s'\nbucket_literal_baseurl = '%s'\n"
                 % (config.bucket_host, config.bucket_user, config.bucket_pass, config.bucket_db, config.bucket_literal_path, config.bucket_literal_baseurl))
        if config.option('do you want to generate bucket tables and populate them with some default data?', True):
            db = MySQLdb.connect(host=config.bucket_host,
                         user=config.bucket_user,
                         passwd=config.bucket_pass,
                         db=config.bucket_db)
            cur = db.cursor()
            #Create facts table
            cur.execute("CREATE TABLE IF NOT EXISTS `bucket_facts` (`id` int(10) unsigned NOT NULL auto_increment, `fact` varchar(128) NOT NULL,`tidbit` text NOT NULL,`verb` varchar(16) NOT NULL default 'is',`RE` tinyint(1) NOT NULL,`protected` tinyint(1) NOT NULL,`mood` tinyint(3) unsigned default NULL,`chance` tinyint(3) unsigned default NULL,PRIMARY KEY (`id`),UNIQUE KEY `fact` (`fact`,`tidbit`(200),`verb`),KEY `trigger` (`fact`),KEY `RE` (`RE`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;")
            #Create inventory table
            cur.execute("CREATE TABLE IF NOT EXISTS `bucket_items` (`id` int(10) unsigned NOT NULL auto_increment,`channel` varchar(64) NOT NULL,`what` varchar(255) NOT NULL,`user` varchar(64) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `what` (`what`),KEY `from` (`user`),KEY `where` (`channel`)) ENGINE=MyISAM DEFAULT CHARSET=latin1 ;")
            #Insert a Don't Know factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('Don\'t Know', '++?????++ Out of Cheese Error. Redo From Start.', '<reply>', False, False, None, None))
            #Insert a pickup full factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('pickup full', 'takes $item but drops $giveitem', '<action>', False, False, None, None))
            #Insert a duplicate item factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('duplicate item', 'No thanks, I\'ve already got $item', '<reply>', False, False, None, None))
            #Insert a take item factiod
            cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', ('takes item', 'Oh, thanks, I\'ll keep this $item safe', '<reply>', False, False, None, None))
            db.commit()
            db.close()
    return chunk
    
class Inventory():
    ''' Everything inventory related '''
    avilable_items = []
    current_items = deque([]) #FIFO. Max length 15
    def add_random(self):
        ''' Adds a random item to the inventory'''
        item = self.avilable_items[randint(0, len(self.avilable_items)-1)].strip()
        if item in self.current_items:
            try:
                return self.add_random()
            except RuntimeError:
                #Too much recursion, this can only mean all avilable_items are in current_items. Bananas.
                self.current_items.appendleft('bananas!')
                return 'bananas!'
        self.current_items.appendleft(item)
        return item
    def add(self, item, user, channel, jenni):
        ''' Adds an item to the inventory'''
        dropped = False
        item = item.strip()
        if item.lower() not in [x.lower() for x in self.avilable_items]:
            db = connect_db(jenni)
            cur = db.cursor()
            try:
                cur.execute('INSERT INTO bucket_items (`channel`, `what`, `user`) VALUES (%s, %s, %s);', (channel, item.encode('utf8'), user))
            except MySQLdb.IntegrityError, e:
                jenni.debug('bucket', 'IntegrityError in inventory code', 'warning')
                jenni.debug('bucket', str(e), 'warning')
            db.commit()
            db.close()
            self.avilable_items.append(item)
        if item in self.current_items:
            return '%ERROR% duplicate item %ERROR%'
        if len(self.current_items) >= 15:
            dropped = True
        self.current_items.appendleft(item)
        return dropped
    def random_item(self):
        ''' returns a random item '''
        if len(self.current_items) == 0:
            return 'bananas!'
        item = self.current_items[randint(0, len(self.current_items)-1)]
        return item
    def give_item(self):
        ''' returns a random item and removes it from the inventory '''
        item = self.random_item()
        try:
            self.current_items.remove(item)
        except ValueError:
            pass
        return item
    def remove(self, item):
        ''' Attemt to remove an item from the inventory, returns False if failed '''
        try:
            self.current_items.remove(item)
            return True
        except ValueError:
            return False

class bucket_runtime_data():
    dont_know_cache = [] #Caching all the Don't Know factoids to reduce amount of DB reads
    what_was_that = {} #Remembering info of last DB read, per channel. for use with the "what was that" command.
    inhibit_reply = '' #Used to inhibit reply of an error message after teaching a factoid
    last_teach = {}
    last_lines = Ddict(dict) #For quotes.
    inventory = None
    shut_up = False
    special_verbs = ['<reply>', '<directreply>', '<directaction>', '<action>', '<alias>']
    inv_steal_match = None #Will contain a compiled regex object to check for inventory 'steal' event
    inv_give_match = None #Will contain a compiled regex object to check for inventory 'give' event

def remove_punctuation(string):
    return sub("[,\.\!\?\;\:]", '', string)
def setup(jenni):
    print 'Setting up Bucket...'
    db = None
    cur = None
    try:
        db = connect_db(jenni)
    except:
        print 'Error connecting to the bucket database.'
        return
    bucket_runtime_data.inventory = Inventory()
    cur = db.cursor()
    #caching "Don't Know" replies
    cur.execute('SELECT * FROM bucket_facts WHERE fact = "Don\'t Know";')
    results = cur.fetchall()
    cur.execute('SELECT * FROM bucket_items;')
    items = cur.fetchall()
    db.close()
    for result in results:
        bucket_runtime_data.dont_know_cache.append(result[2])
    for item in items:
        bucket_runtime_data.inventory.avilable_items.append(item[2])
    bucket_runtime_data.inv_give_match = re.compile('((^\001ACTION (gives|hands) %s)|^%s. take this) (.*)' % (jenni.nick, jenni.nick), re.I)
    bucket_runtime_data.inv_steal_match = re.compile('^\001ACTION (steals|takes) %s\'s (.*)' % jenni.nick, re.I)
    print 'Done setting up Bucket!'
def add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance):
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('INSERT INTO bucket_facts (`fact`, `tidbit`, `verb`, `RE`, `protected`, `mood`, `chance`) VALUES (%s, %s, %s, %s, %s, %s, %s);', (fact, tidbit, verb, re, protected, mood, chance))
        db.commit()
    except MySQLdb.IntegrityError:
        jenni.say("I already had it that way!")
        return False
    finally:
        db.close()
    bucket_runtime_data.last_teach[trigger.sender] = [fact,verb,tidbit]
    jenni.say("Okay, "+trigger.nick)

def teach_is_are(jenni, trigger):
    """Teaches a is b and a are b"""
    fact = trigger.group(1)
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    fact = remove_punctuation(fact)
    tidbit = trigger.group(3)
    verb = trigger.group(2)
    re = False
    protected = False
    mood = None
    chance = None
    for word in trigger.group(0).lower().split(' '):
        if word in bucket_runtime_data.special_verbs:
            return #do NOT teach is are if the trigger is similar to "Lemon, Who is the king of the derps? <reply> I am!" (contains both is|are and a special verb.
    
    add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
teach_is_are.rule = ('$nick', '(.*?) (is|are) (.*)')
teach_is_are.priority = 'high'

def teach_verb(jenni, trigger):
    """Teaches verbs/ambiguous reply"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    fact = trigger.group(1)
    fact = remove_punctuation(fact)
    tidbit = trigger.group(3)
    verb = trigger.group(2)
    re = False
    protected = False
    mood = None
    chance = None
    
    if verb not in bucket_runtime_data.special_verbs:
        verb = verb[1:-1]
    
    if fact == tidbit and verb == '<alias>':
        jenni.reply('You can\'t alias like this!')
        return
    add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
    if fact.lower() == 'don\'t know':
        bucket_runtime_data.dont_know_cache.append(tidbit)
teach_verb.rule = ('$nick', '(.*?) (<\S+>) (.*)')
teach_verb.priority = 'high'

def save_quote(jenni, trigger):
    """Saves a quote"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    quotee = trigger.group(1).lower()
    word = trigger.group(2)
    fact = quotee+' quotes'
    verb = '<reply>'
    re = False
    protected = False
    mood = None
    chance = None
    try:
        memory = bucket_runtime_data.last_lines[trigger.sender][quotee]
    except KeyError:
        jenni.say("Sorry, I don't remember what %s said about %s" % (quotee, word))
        return
    for line in memory:
        if remove_punctuation(word.lower()) in remove_punctuation(line.lower()):
            if line.startswith('\001ACTION'):
                line = line[len('\001ACTION '):-1]
                tidbit = '* %s %s' % (quotee, line)
            else:
                tidbit = '<%s> %s' % (quotee, line)
            add_fact(jenni, trigger, fact, tidbit, verb, re, protected, mood, chance)
            jenni.say("Remembered that %s <reply> %s" % (fact, tidbit))
            return
    jenni.say("Sorry, I don't remember what %s said about %s" % (quotee, word))
save_quote.rule = ('$nick', 'remember (.*?) (.*)')
save_quote.priority = 'high'

def delete_factoid(jenni, trigger):
    """Delets a factoid"""
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    was = bucket_runtime_data.what_was_that
    if not trigger.admin:
        was[trigger.sender] = dont_know(jenni)
        return
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('SELECT * FROM bucket_facts WHERE ID = %s;', int(trigger.group(1)))
        results = cur.fetchall()
        if len(results)>1:
            jenni.debug('bucket', 'More than one factoid with the same ID?', 'warning')
            jenni.debug('bucket', str(results), 'warning')
            jenni.say('More than one factoid with the same ID. I refuse to continue.')
            return
        elif len(results) == 0:
            jenni.reply('No such factoid')
            return
        cur.execute('DELETE FROM bucket_facts WHERE ID = %s',int(trigger.group(1)))
        db.commit()
    except:
        jenni.say("Delete failed! are you sure this is a valid factoid ID?")
        return
    finally:
        db.close()
    line = results[0]
    fact, tidbit, verb = parse_factoid(line)
    jenni.say("Okay, %s, forgot that %s %s %s" % (trigger.nick, fact, verb, tidbit))
    
delete_factoid.rule = ('$nick', 'delete #(.*)')
delete_factoid.priority = 'high'

def undo_teach(jenni, trigger):
    """Undo teaching factoid"""
    was = bucket_runtime_data.what_was_that
    bucket_runtime_data.inhibit_reply = trigger.group(0)
    if not trigger.admin:
        was[trigger.sender] = dont_know(jenni)
        return
    last_teach = bucket_runtime_data.last_teach
    fact = ''
    verb = ''
    tidbit = ''
    try:
        fact = last_teach[trigger.sender][0]
        verb = last_teach[trigger.sender][1]
        tidbit = last_teach[trigger.sender][2]
    except KeyError:
        jenni.reply('Nothing to undo!')
        return
    db = None
    cur = None
    db = connect_db(jenni)
    cur = db.cursor()
    try:
        cur.execute('DELETE FROM bucket_facts WHERE `fact` = %s AND `verb` = %s AND `tidbit` = %s', (fact, verb, tidbit))
        db.commit()
    except:
        jenni.say("Undo failed, this shouldn't have happened!")
        return
    finally:
        db.close()
    jenni.say("Okay, %s, forgot that %s %s %s" % (trigger.nick, fact, verb, tidbit))
    
undo_teach.rule = ('$nick', 'undo last')
undo_teach.priority = 'high'

def say_fact(jenni, trigger):
    """Response, if needed"""
    query = trigger.group(0)
    was = bucket_runtime_data.what_was_that
    inventory = bucket_runtime_data.inventory
    db = None
    cur = None
    results = None
    inv_give_match = bucket_runtime_data.inv_give_match.search(query)
    if inv_give_match is not None:
        item = inv_give_match.group(4)
        if item.endswith('\001'):
            item = item[:-1]
        item = item.strip()
        dropped = inventory.add(item, trigger.nick, trigger.sender, jenni)
        db = connect_db(jenni)
        cur = db.cursor()
        search_term = ''
        if dropped == False:
            #Query for 'takes item'
            search_term = 'takes item'
        elif dropped == '%ERROR% duplicate item %ERROR%':
            #Query for 'duplicate item'
            search_term = 'duplicate item'
        else:
            #Query for 'pickup full'
            search_term = 'pickup full'
        cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
        results = cur.fetchall()
        result = pick_result(results, jenni)
        fact, tidbit, verb = parse_factoid(result)
        tidbit = tidbit.replace('$item', item)
        tidbit = tidbit_vars(tidbit, trigger, False)

        if verb not in bucket_runtime_data.special_verbs:
            jenni.say("%s %s %s" % (fact, verb, tidbit))
        elif verb == '<reply>':
            jenni.say(tidbit)
        elif verb == '<action>':
            jenni.action(tidbit)
        was = result
        return
    inv_steal_match = bucket_runtime_data.inv_steal_match.search(query)
    if inv_steal_match is not None:
        item = inv_steal_match.group(2)
        if item.endswith('\001'):
            item = item[:-1]
        if (inventory.remove(item)):
            jenni.say('Hey! Give it back, it\'s mine!')
        else:
            jenni.say('But I don\'t have any %s' % item)
        return
    if query.startswith('\001ACTION'):
        query = query[len('\001ACTION '):]
    addressed = query.lower().startswith(jenni.nick.lower()) #Check if our nick was mentioned
    search_term = query.lower().strip() #What we are going to pass to MySql as our search term
    if addressed:
        search_term = search_term[(len(jenni.nick)+1):].strip() #Remove our nickname from the search term
    search_term = remove_punctuation(search_term).strip()

    if len(query) < 6 and not addressed:
        return #Ignore factoids shorter than 6 chars when not addressed
    if search_term == 'don\'t know' and not addressed:
        return #Ignore "don't know" when not addressed
    if not addressed and bucket_runtime_data.shut_up:
        return #Don't say anything if not addressed and shutting up
    if search_term == 'shut up' and addressed:
        jenni.reply('Okay...')
        bucket_runtime_data.shut_up = True
        return
    elif search_term == 'come back' and addressed:
        jenni.reply('I\'m back!')
        bucket_runtime_data.shut_up = False
        return
    literal = False
    inhibit = bucket_runtime_data.inhibit_reply
    if search_term.startswith('literal '):
        literal = True
        search_term = str.replace(search_term, 'literal ','')
    elif search_term == 'what was that' and addressed:
        try:
            jenni.say('That was '+ was[trigger.sender])
        except KeyError:
            jenni.say('I have no idea')
        return
    elif search_term.startswith('reload') or search_term.startswith('update') or inhibit == search_term or inhibit == trigger.group(0):
        return #ignore commands such as reload or update, don't show 'Don't Know' responses for these 

    db = connect_db(jenni)
    cur = db.cursor()

    try:
        if search_term == 'random quote':
            cur.execute('SELECT * FROM bucket_facts WHERE fact LIKE "% quotes";')
            results = cur.fetchall()
        else:
            cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
            results = cur.fetchall()
    except UnicodeEncodeError, e:
        jenni.debug('bucket','Warning, database encoding error', 'warning')
        jenni.debug('bucket', e, 'warning')
    finally:
        db.close()
    if results == None:
        return
    result = pick_result(results, jenni)
    if addressed and result == None:
        was[trigger.sender] = 'Don\'t know: '+dont_know(jenni)
        return
    elif result == None:
        return
            
    fact, tidbit, verb = parse_factoid(result)
    tidbit = tidbit_vars(tidbit, trigger)

    if literal:
        if len(results) == 1:
            result = results[0]
            number = int(result[0])
            fact, tidbit, verb = parse_factoid(result)
            jenni.say ("#%d - %s %s %s" % (number, fact, verb, tidbit))
        else:
            jenni.reply('just a second, I\'ll make the list!')
            bucket_literal_path = jenni.config.bucket_literal_path
            bucket_literal_baseurl = jenni.config.bucket_literal_baseurl
            if not bucket_literal_baseurl.endswith('/'):
                bucket_literal_baseurl = bucket_literal_baseurl + '/'
            if not os.path.isdir(bucket_literal_path):
                try:
                    os.makedirs(bucket_literal_path)
                except Exception as e:
                    jenni.say("Can't create directory to store literal, sorry!")
                    jenni.say(e)
                    return
            f = open(os.path.join(bucket_literal_path, fact.lower()+'.txt'), 'w')
            for result in results:
                number = int(result[0])
                fact, tidbit, verb = parse_factoid(result)
                literal_line = "#%d - %s %s %s" % (number, fact, verb, tidbit)
                f.write(literal_line+'\n')
            f.close()
            jenni.reply('Here you go! %s (%d factoids)' % (bucket_literal_baseurl+web.quote(fact.lower()+'.txt'), len(results)))
        result = 'Me giving you a literal link'
    elif verb not in bucket_runtime_data.special_verbs:
        jenni.say("%s %s %s" % (fact, verb, tidbit))
    elif verb == '<reply>':
        jenni.say(tidbit)
    elif verb == '<action>':
        jenni.action(tidbit)
    elif verb == '<directreply>' and addressed:
        jenni.say(tidbit)
    elif verb == '<directaction>' and addressed:
        jenni.action(tidbit)
    was[trigger.sender] = result
    
    
say_fact.rule = ('(.*)')
say_fact.priority = 'low'

def pick_result(results, jenni):
    try:
        if len(results) == 1:
            result = results[0]
        elif len(results) > 1:
            result = results[randint(0, len(results)-1)]
        elif len(results) == 0:
            return None
        if result[3] == '<alias>':
            #Handle alias, recursive!
            db = connect_db(jenni)
            cur = db.cursor()
            search_term = result[2].strip()
            try:
                cur.execute('SELECT * FROM bucket_facts WHERE fact = %s;', search_term)
                results = cur.fetchall()
            except UnicodeEncodeError, e:
                jenni.debug('bucket','Warning, database encoding error', 'warning')
                jenni.debug('bucket',e , 'warning')
            finally:
                db.close()
            result = pick_result(results, jenni)
        return result
    except RuntimeError, e:
        jenni.debug('bucket', 'RutimeError in pick_result', 'warning')
        jenni.debug('bucket', e, 'warning')
        return None

def get_inventory(jenni, trigger):
    ''' get a human readable list of the bucket inventory '''

    bucket_runtime_data.inhibit_reply = trigger.group(0)

    inventory = bucket_runtime_data.inventory

    readable_item_list = ''
    
    if len(inventory.current_items)==0:
        return jenni.action('is carrying nothing')
    for item in inventory.current_items:
        readable_item_list = readable_item_list + ' '+item.encode('utf8')+','

    jenni.action('is carrying'+readable_item_list)

get_inventory.rule = ('$nick','inventory')
get_inventory.priority = 'medium'

def connect_db(jenni):
    return MySQLdb.connect(host=jenni.config.bucket_host,
                         user=jenni.config.bucket_user,
                         passwd=jenni.config.bucket_pass,
                         db=jenni.config.bucket_db,
                         charset="utf8",
                         use_unicode=True)
def tidbit_vars(tidbit, trigger, random_item=True):
    ''' Parse in-tidbit vars '''
    #Special in-tidbit vars:
    inventory = bucket_runtime_data.inventory
{'fraction_non_alphanumeric': 0.12611543364543495, 'fraction_numerical': 0.005340894105235395, 'mean_word_length': 2.6159103552411986, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 61, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4847142', 'n_tokens_mistral': 14333, 'n_tokens_neox': 13940, 'n_words': 3917}