bgneal@339: #! /usr/bin/env python
bgneal@339: """sg101Bot.py
bgneal@339: 
bgneal@339: IRC Bot for SurfGuitar101.com's IRC server.
bgneal@339: This bot watches who is in the given channel and updates a MySQL
bgneal@339: database accordingly. The database is read by the website to display
bgneal@339: who is in the channel.
bgneal@339: 
bgneal@339: Brian Neal <brian@surfguitar101.com>
bgneal@339: """
bgneal@339: 
bgneal@339: import re
bgneal@339: import sys
bgneal@339: import random
bgneal@339: import logging
bgneal@339: from optparse import OptionParser
bgneal@339: import ConfigParser
bgneal@339: 
bgneal@339: import MySQLdb
bgneal@339: import irclib
bgneal@339: from daemon import Daemon
bgneal@339: 
bgneal@339: #irclib.DEBUG = True
bgneal@339: 
bgneal@339: class QuoteDb(object):
bgneal@339:    """QuoteDb is a class that reads a quote file and provdes a random quote when asked"""
bgneal@339: 
bgneal@339:    def __init__(self, filename):
bgneal@339:       random.seed()
bgneal@339:       f = open(filename, 'r')
bgneal@339:       self.quotes = []
bgneal@339:       for line in f:
bgneal@339:          self.quotes.append(line.strip())
bgneal@339:       f.close()
bgneal@339: 
bgneal@339:    def get(self):
bgneal@339:       return random.choice(self.quotes)
bgneal@339:       
bgneal@339: 
bgneal@339: class Sg101Table(object):
bgneal@339:    """Sg101Table class for abstracting the database table that is used to store who is in the channel"""
bgneal@339:    
bgneal@339:    def __init__(self, dbHost, dbName, dbUser, dbPassword, tableName):
bgneal@339:       self.dbArgs = {'host' : dbHost, 'user' : dbUser, 'passwd' : dbPassword, 'db' : dbName}
bgneal@339:       self.table = tableName
bgneal@339:       self.dbRefCnt = 0
bgneal@339: 
bgneal@339:    def empty(self):
bgneal@339:       self.__dbOpen()
bgneal@339:       """empties the table of channel users"""
bgneal@339:       logging.debug('emptying the table')
bgneal@339:       self.cursor.execute('TRUNCATE TABLE ' + self.table)
bgneal@339:       self.__dbClose()
bgneal@339: 
bgneal@339:    def add(self, nick):
bgneal@339:       self.__dbOpen()
bgneal@339:       """adds the nickname to the table"""
bgneal@339:       logging.debug('adding to the table: ' + nick)
bgneal@339:       self.cursor.execute("INSERT INTO " + self.table +
bgneal@339:           " (name, last_update) VALUES (%s, NOW())", nick)
bgneal@339:       self.__dbClose()
bgneal@339: 
bgneal@339:    def remove(self, nick):
bgneal@339:       self.__dbOpen()
bgneal@339:       """removes the nickname from the table"""
bgneal@339:       logging.debug('removing from the table: ' + nick)
bgneal@339:       self.cursor.execute("DELETE FROM " + self.table + " WHERE `name` = %s", nick)
bgneal@339:       self.__dbClose()
bgneal@339: 
bgneal@339:    def set(self, nicks):
bgneal@339:       self.__dbOpen()
bgneal@339:       """empties and then adds all the nicknames in the nicks list to the table"""
bgneal@339:       self.empty()
bgneal@339:       logging.debug('setting the table: ' + ', '.join(nicks))
bgneal@339:       for nick in nicks:
bgneal@339:          self.add(nick)
bgneal@339:       self.__dbClose()
bgneal@339: 
bgneal@339:    def __dbOpen(self):
bgneal@339:       if self.dbRefCnt <= 0:
bgneal@339:          self.connection = MySQLdb.connect(
bgneal@339:             host = self.dbArgs['host'],
bgneal@339:             user = self.dbArgs['user'],
bgneal@339:             passwd = self.dbArgs['passwd'],
bgneal@339:             db = self.dbArgs['db'])
bgneal@339:          self.cursor = self.connection.cursor()
bgneal@339:          self.dbRefCnt = 1
bgneal@339:       else:
bgneal@339:          self.dbRefCnt = self.dbRefCnt + 1
bgneal@339: 
bgneal@339:    def __dbClose(self):
bgneal@339:       self.dbRefCnt = self.dbRefCnt - 1
bgneal@339:       if self.dbRefCnt <= 0:
bgneal@339:          self.cursor.close()
bgneal@339:          self.connection.commit()
bgneal@339:          self.connection.close()
bgneal@339:          self.dbRefCnt = 0
bgneal@339:       
bgneal@339: 
bgneal@339: class Sg101Bot(irclib.SimpleIRCClient):
bgneal@339:    """Sg101Bot class for monitoring a given channel and updating a database table accordingly"""
bgneal@339: 
bgneal@339:    def __init__(self, channel, nickname, server, port, password, db, quotes, quoteMod = 5):
bgneal@339:       """__init__ function for Sg101Bot"""
bgneal@339: 
bgneal@339:       irclib.SimpleIRCClient.__init__(self)
bgneal@339:       self.channel = channel
bgneal@339:       self.nickname = nickname
bgneal@339:       self.server = server
bgneal@339:       self.port = port
bgneal@339:       self.password = password
bgneal@339:       self.users = []
bgneal@339:       self.db = db
bgneal@339:       self.quotes = quotes
bgneal@339:       self.quoteMod = quoteMod
bgneal@339:       self.pingCount = 0;
bgneal@339: 
bgneal@339:    def start(self):
bgneal@339:       """call this function to start the bot"""
bgneal@339:       self.db.empty()
bgneal@339:       self.__connect()
bgneal@339:       irclib.SimpleIRCClient.start(self)
bgneal@339: 
bgneal@339:    def on_nicknameinuse(self, c, e):
bgneal@339:       """called if nick is in use; attempts to change nick"""
bgneal@339:       c.nick(c.get_nickname() + "_")
bgneal@339: 
bgneal@339:    def on_welcome(self, c, e):
bgneal@339:       """called when welcomed to the IRC server; attempts to join channel"""
bgneal@339:       c.join(self.channel)
bgneal@339: 
bgneal@339:    def on_namreply(self, c, e):
bgneal@339:       """called when we hear a namreply; we scan the names and if different than our
bgneal@339:       info of who is in the channel we update the database"""
bgneal@339:       me = c.get_nickname()
bgneal@339:       users = [re.sub('^[@\+]', '', u) for u in e.arguments()[2].split(' ') if u != me]
bgneal@339:       users.sort()
bgneal@339:       if (self.users != users):
bgneal@339:          logging.debug('on_namreply: detecting user list difference')
bgneal@339:          self.users = users
bgneal@339:          self.db.set(users)
bgneal@339:          self.__printUsers()
bgneal@339: 
bgneal@339:    def on_join(self, c, e):
bgneal@339:       """called after we or someone else joins the channel; update our list of users and db"""
bgneal@339:       nick = irclib.nm_to_n(e.source())
bgneal@339:       if nick != c.get_nickname() and nick not in self.users:
bgneal@339:          self.__addUser(nick)
bgneal@339:          self.__printUsers()
bgneal@339: 
bgneal@339:    def on_part(self, c, e):
bgneal@339:       """called after we or someone else parts the channel; update our list of users and db"""
bgneal@339:       nick = irclib.nm_to_n(e.source())
bgneal@339:       if nick != c.get_nickname():
bgneal@339:          self.__delUser(nick)
bgneal@339:          self.__printUsers()
bgneal@339: 
bgneal@339:    def on_kick(self, c, e):
bgneal@339:       """called after someone is kicked; update our list of users and db"""
bgneal@339:       nick = e.arguments()[0]
bgneal@339:       if nick != c.get_nickname():
bgneal@339:          self.__delUser(nick)
bgneal@339:          self.__printUsers()
bgneal@339: 
bgneal@339:    def on_quit(self, c, e):
bgneal@339:       """called after someone quits; update our list of users and db"""
bgneal@339:       nick = irclib.nm_to_n(e.source())
bgneal@339:       if nick != c.get_nickname():
bgneal@339:          self.__delUser(nick)
bgneal@339:          self.__printUsers()
bgneal@339: 
bgneal@339:    def on_ping(self, c, e):
bgneal@339:       """called when we are pinged; we use this opportunity to ask who is in the channel via NAMES"""
bgneal@339:       c.names()
bgneal@339:       self.pingCount = self.pingCount + 1
bgneal@339:       if (self.quoteMod > 0) and len(self.users) > 0 and (self.pingCount % self.quoteMod) == 0:
bgneal@339:          c.privmsg(self.channel, self.quotes.get())
bgneal@339: 
bgneal@339:    def on_privmsg(self, c, e):
bgneal@339:       self.doCommand(e, e.arguments()[0], e.source())
bgneal@339: 
bgneal@339:    def on_pubmsg(self, c, e):
bgneal@339:       a = e.arguments()[0].split(':', 1)
bgneal@339:       if len(a) > 1 and irclib.irc_lower(a[0]) == irclib.irc_lower(c.get_nickname()):
bgneal@339:          self.doCommand(e, a[1].strip(), e.target())
bgneal@339: 
bgneal@339:    def on_disconnect(self, c, e):
bgneal@339:       logging.critical('received on_disconnect()')
bgneal@339:       logging.debug('scheduling a routine in 30 secs')
bgneal@339:       self.users = []
bgneal@339:       self.db.empty()
bgneal@339:       self.connection.execute_delayed(30, self.__connectedChecker)
bgneal@339: 
bgneal@339:    def __connectedChecker():
bgneal@339:       logging.debug('__connectedChecker()')
bgneal@339:       if not self.connection.is_connected():
bgneal@339:          logging.debug('rescheduling __connectedChecker in 30 secs')
bgneal@339:          self.connection.execute_delayed(30, self._connected_checker)
bgneal@339:          self.__connect()
bgneal@339: 
bgneal@339: 
bgneal@339:    def doCommand(self, e, cmd, reply):
bgneal@339:       if cmd == 'yow':
bgneal@339:          self.connection.privmsg(reply, self.quotes.get())
bgneal@339: 
bgneal@339:    def __connect(self):
bgneal@339:       """our internal connect function"""
bgneal@339:       self.connect(self.server,
bgneal@339:                    self.port,
bgneal@339:                    self.nickname,
bgneal@339:                    self.password)
bgneal@339: 
bgneal@339:    def __printUsers(self):
bgneal@339:       """internal print users command"""
bgneal@339:       msg = 'My users are: (' + ', '.join(self.users) +  ')'
bgneal@339:       logging.debug(msg)
bgneal@339: 
bgneal@339:    def __addUser(self, nick):
bgneal@339:       """adds a user to our list and db"""
bgneal@339:       self.users.append(nick)
bgneal@339:       self.users.sort()
bgneal@339:       self.db.add(nick)
bgneal@339: 
bgneal@339:    def __delUser(self, nick):
bgneal@339:       """removes a user from our list and db"""
bgneal@339:       if nick in self.users:
bgneal@339:          self.users.remove(nick)
bgneal@339:       self.db.remove(nick)
bgneal@339: 
bgneal@339: 
bgneal@339: class BotDaemon(Daemon):
bgneal@339:     def __init__(self, filename, options, db_config):
bgneal@339:         Daemon.__init__(self, filename)
bgneal@339:         self.options = options
bgneal@339:         self.db_config = db_config
bgneal@339: 
bgneal@339:     def run(self):
bgneal@339:         logging.basicConfig(level = logging.DEBUG,
bgneal@339:                            format = '%(asctime)s %(levelname)s %(message)s',
bgneal@339:                            filename = '/home/brian/irc/sg101Bot.log',
bgneal@339:                            filemode = 'w')
bgneal@339:         logging.info('sg101Bot starting')
bgneal@339: 
bgneal@339:         server = self.options.server
bgneal@339:         port = self.options.port
bgneal@339:         channel = self.options.channel
bgneal@339:         nickname = self.options.nick
bgneal@339:         password = self.options.password
bgneal@339: 
bgneal@339:         try:
bgneal@339:           quotes = QuoteDb('/home/brian/irc/zippy.txt')
bgneal@339:           db = Sg101Table(self.db_config['host'],
bgneal@339:                   self.db_config['database'],
bgneal@339:                   self.db_config['user'],
bgneal@339:                   self.db_config['password'],
bgneal@339:                   self.db_config['table'])
bgneal@339:           bot = Sg101Bot(channel, nickname, server, port, password, db, quotes, 0)
bgneal@339:           bot.start()
bgneal@339:         except MySQLdb.OperationalError, message:
bgneal@339:           logging.exception('MySQLdb.OperationalError: %d %s' % (message[0], message[1]))
bgneal@339:         except MySQLdb.ProgrammingError, message:
bgneal@339:           logging.exception('MySQLdb.ProgrammingError: %d %s' % (message[0], message[1]))
bgneal@339:         except:
bgneal@339:            print "Got an exception:", sys.exc_info()[0]
bgneal@339:            logging.exception('Exception received ' + str(sys.exc_info()))
bgneal@339: 
bgneal@339:         logging.info('sg101Bot ending')
bgneal@339: 
bgneal@339: if __name__ == "__main__":
bgneal@339:     parser = OptionParser(usage='usage: %prog [options] start|stop|restart',
bgneal@339:          description="""\
bgneal@339:     SG101 IRC Bot. Monitors who is in the specified channel and
bgneal@339:     updates a database table accordingly for display by the website.
bgneal@339:     """)
bgneal@339: 
bgneal@339:     parser.add_option("-s", "--server", default="localhost",
bgneal@339:          help="server address")
bgneal@339:     parser.add_option("-p", "--port", type="int", default=6667,
bgneal@339:          help="port number")
bgneal@339:     parser.add_option("-c", "--channel", default="#ShallowEnd",
bgneal@339:          help="channel name")
bgneal@339:     parser.add_option("-n", "--nick", default="bot",
bgneal@339:          help="bot nickname")
bgneal@339:     parser.add_option("-w", "--password", default="morereverb",
bgneal@339:          help="channel password")
bgneal@339:     parser.add_option("-q", "--quote", default="/home/brian/irc/zippy.txt",
bgneal@339:          help="Quote file")
bgneal@339:     (options, args) = parser.parse_args()
bgneal@339: 
bgneal@339:     commands = ('start', 'stop', 'restart')
bgneal@339:     if len(args) != 1 and args[0] not in commands:
bgneal@339:         parser.print_help()
bgneal@339:         sys.exit("Please provide a single command: start, stop, or restart.")
bgneal@339: 
bgneal@339:     config = ConfigParser.ConfigParser()
bgneal@339:     config.read('/home/brian/irc/sg101Bot.ini')
bgneal@339:     db_config = dict(config.items('Database'))
bgneal@339: 
bgneal@339:     daemon = BotDaemon('/tmp/sg101Bot.pid', options, db_config)
bgneal@339:     cmd = args[0]
bgneal@339:     if cmd == 'start':
bgneal@339:         daemon.start()
bgneal@339:     elif cmd == 'stop':
bgneal@339:         daemon.stop()
bgneal@339:     elif cmd == 'restart':
bgneal@339:         daemon.restart()