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 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()