comparison tools/sg101Bot.py @ 339:b871892264f2

Adding the sg101 IRC bot code to SVN. This code is pretty rough and needs love, but it gets the job done (one of my first Python apps). This fixes #150.
author Brian Neal <bgneal@gmail.com>
date Sat, 26 Feb 2011 21:27:49 +0000
parents
children
comparison
equal deleted inserted replaced
338:e28aee19f7c9 339:b871892264f2
1 #! /usr/bin/env python
2 """sg101Bot.py
3
4 IRC Bot for SurfGuitar101.com's IRC server.
5 This bot watches who is in the given channel and updates a MySQL
6 database accordingly. The database is read by the website to display
7 who is in the channel.
8
9 Brian Neal <brian@surfguitar101.com>
10 """
11
12 import re
13 import sys
14 import random
15 import logging
16 from optparse import OptionParser
17 import ConfigParser
18
19 import MySQLdb
20 import irclib
21 from daemon import Daemon
22
23 #irclib.DEBUG = True
24
25 class QuoteDb(object):
26 """QuoteDb is a class that reads a quote file and provdes a random quote when asked"""
27
28 def __init__(self, filename):
29 random.seed()
30 f = open(filename, 'r')
31 self.quotes = []
32 for line in f:
33 self.quotes.append(line.strip())
34 f.close()
35
36 def get(self):
37 return random.choice(self.quotes)
38
39
40 class Sg101Table(object):
41 """Sg101Table class for abstracting the database table that is used to store who is in the channel"""
42
43 def __init__(self, dbHost, dbName, dbUser, dbPassword, tableName):
44 self.dbArgs = {'host' : dbHost, 'user' : dbUser, 'passwd' : dbPassword, 'db' : dbName}
45 self.table = tableName
46 self.dbRefCnt = 0
47
48 def empty(self):
49 self.__dbOpen()
50 """empties the table of channel users"""
51 logging.debug('emptying the table')
52 self.cursor.execute('TRUNCATE TABLE ' + self.table)
53 self.__dbClose()
54
55 def add(self, nick):
56 self.__dbOpen()
57 """adds the nickname to the table"""
58 logging.debug('adding to the table: ' + nick)
59 self.cursor.execute("INSERT INTO " + self.table +
60 " (name, last_update) VALUES (%s, NOW())", nick)
61 self.__dbClose()
62
63 def remove(self, nick):
64 self.__dbOpen()
65 """removes the nickname from the table"""
66 logging.debug('removing from the table: ' + nick)
67 self.cursor.execute("DELETE FROM " + self.table + " WHERE `name` = %s", nick)
68 self.__dbClose()
69
70 def set(self, nicks):
71 self.__dbOpen()
72 """empties and then adds all the nicknames in the nicks list to the table"""
73 self.empty()
74 logging.debug('setting the table: ' + ', '.join(nicks))
75 for nick in nicks:
76 self.add(nick)
77 self.__dbClose()
78
79 def __dbOpen(self):
80 if self.dbRefCnt <= 0:
81 self.connection = MySQLdb.connect(
82 host = self.dbArgs['host'],
83 user = self.dbArgs['user'],
84 passwd = self.dbArgs['passwd'],
85 db = self.dbArgs['db'])
86 self.cursor = self.connection.cursor()
87 self.dbRefCnt = 1
88 else:
89 self.dbRefCnt = self.dbRefCnt + 1
90
91 def __dbClose(self):
92 self.dbRefCnt = self.dbRefCnt - 1
93 if self.dbRefCnt <= 0:
94 self.cursor.close()
95 self.connection.commit()
96 self.connection.close()
97 self.dbRefCnt = 0
98
99
100 class Sg101Bot(irclib.SimpleIRCClient):
101 """Sg101Bot class for monitoring a given channel and updating a database table accordingly"""
102
103 def __init__(self, channel, nickname, server, port, password, db, quotes, quoteMod = 5):
104 """__init__ function for Sg101Bot"""
105
106 irclib.SimpleIRCClient.__init__(self)
107 self.channel = channel
108 self.nickname = nickname
109 self.server = server
110 self.port = port
111 self.password = password
112 self.users = []
113 self.db = db
114 self.quotes = quotes
115 self.quoteMod = quoteMod
116 self.pingCount = 0;
117
118 def start(self):
119 """call this function to start the bot"""
120 self.db.empty()
121 self.__connect()
122 irclib.SimpleIRCClient.start(self)
123
124 def on_nicknameinuse(self, c, e):
125 """called if nick is in use; attempts to change nick"""
126 c.nick(c.get_nickname() + "_")
127
128 def on_welcome(self, c, e):
129 """called when welcomed to the IRC server; attempts to join channel"""
130 c.join(self.channel)
131
132 def on_namreply(self, c, e):
133 """called when we hear a namreply; we scan the names and if different than our
134 info of who is in the channel we update the database"""
135 me = c.get_nickname()
136 users = [re.sub('^[@\+]', '', u) for u in e.arguments()[2].split(' ') if u != me]
137 users.sort()
138 if (self.users != users):
139 logging.debug('on_namreply: detecting user list difference')
140 self.users = users
141 self.db.set(users)
142 self.__printUsers()
143
144 def on_join(self, c, e):
145 """called after we or someone else joins the channel; update our list of users and db"""
146 nick = irclib.nm_to_n(e.source())
147 if nick != c.get_nickname() and nick not in self.users:
148 self.__addUser(nick)
149 self.__printUsers()
150
151 def on_part(self, c, e):
152 """called after we or someone else parts the channel; update our list of users and db"""
153 nick = irclib.nm_to_n(e.source())
154 if nick != c.get_nickname():
155 self.__delUser(nick)
156 self.__printUsers()
157
158 def on_kick(self, c, e):
159 """called after someone is kicked; update our list of users and db"""
160 nick = e.arguments()[0]
161 if nick != c.get_nickname():
162 self.__delUser(nick)
163 self.__printUsers()
164
165 def on_quit(self, c, e):
166 """called after someone quits; update our list of users and db"""
167 nick = irclib.nm_to_n(e.source())
168 if nick != c.get_nickname():
169 self.__delUser(nick)
170 self.__printUsers()
171
172 def on_ping(self, c, e):
173 """called when we are pinged; we use this opportunity to ask who is in the channel via NAMES"""
174 c.names()
175 self.pingCount = self.pingCount + 1
176 if (self.quoteMod > 0) and len(self.users) > 0 and (self.pingCount % self.quoteMod) == 0:
177 c.privmsg(self.channel, self.quotes.get())
178
179 def on_privmsg(self, c, e):
180 self.doCommand(e, e.arguments()[0], e.source())
181
182 def on_pubmsg(self, c, e):
183 a = e.arguments()[0].split(':', 1)
184 if len(a) > 1 and irclib.irc_lower(a[0]) == irclib.irc_lower(c.get_nickname()):
185 self.doCommand(e, a[1].strip(), e.target())
186
187 def on_disconnect(self, c, e):
188 logging.critical('received on_disconnect()')
189 logging.debug('scheduling a routine in 30 secs')
190 self.users = []
191 self.db.empty()
192 self.connection.execute_delayed(30, self.__connectedChecker)
193
194 def __connectedChecker():
195 logging.debug('__connectedChecker()')
196 if not self.connection.is_connected():
197 logging.debug('rescheduling __connectedChecker in 30 secs')
198 self.connection.execute_delayed(30, self._connected_checker)
199 self.__connect()
200
201
202 def doCommand(self, e, cmd, reply):
203 if cmd == 'yow':
204 self.connection.privmsg(reply, self.quotes.get())
205
206 def __connect(self):
207 """our internal connect function"""
208 self.connect(self.server,
209 self.port,
210 self.nickname,
211 self.password)
212
213 def __printUsers(self):
214 """internal print users command"""
215 msg = 'My users are: (' + ', '.join(self.users) + ')'
216 logging.debug(msg)
217
218 def __addUser(self, nick):
219 """adds a user to our list and db"""
220 self.users.append(nick)
221 self.users.sort()
222 self.db.add(nick)
223
224 def __delUser(self, nick):
225 """removes a user from our list and db"""
226 if nick in self.users:
227 self.users.remove(nick)
228 self.db.remove(nick)
229
230
231 class BotDaemon(Daemon):
232 def __init__(self, filename, options, db_config):
233 Daemon.__init__(self, filename)
234 self.options = options
235 self.db_config = db_config
236
237 def run(self):
238 logging.basicConfig(level = logging.DEBUG,
239 format = '%(asctime)s %(levelname)s %(message)s',
240 filename = '/home/brian/irc/sg101Bot.log',
241 filemode = 'w')
242 logging.info('sg101Bot starting')
243
244 server = self.options.server
245 port = self.options.port
246 channel = self.options.channel
247 nickname = self.options.nick
248 password = self.options.password
249
250 try:
251 quotes = QuoteDb('/home/brian/irc/zippy.txt')
252 db = Sg101Table(self.db_config['host'],
253 self.db_config['database'],
254 self.db_config['user'],
255 self.db_config['password'],
256 self.db_config['table'])
257 bot = Sg101Bot(channel, nickname, server, port, password, db, quotes, 0)
258 bot.start()
259 except MySQLdb.OperationalError, message:
260 logging.exception('MySQLdb.OperationalError: %d %s' % (message[0], message[1]))
261 except MySQLdb.ProgrammingError, message:
262 logging.exception('MySQLdb.ProgrammingError: %d %s' % (message[0], message[1]))
263 except:
264 print "Got an exception:", sys.exc_info()[0]
265 logging.exception('Exception received ' + str(sys.exc_info()))
266
267 logging.info('sg101Bot ending')
268
269 if __name__ == "__main__":
270 parser = OptionParser(usage='usage: %prog [options] start|stop|restart',
271 description="""\
272 SG101 IRC Bot. Monitors who is in the specified channel and
273 updates a database table accordingly for display by the website.
274 """)
275
276 parser.add_option("-s", "--server", default="localhost",
277 help="server address")
278 parser.add_option("-p", "--port", type="int", default=6667,
279 help="port number")
280 parser.add_option("-c", "--channel", default="#ShallowEnd",
281 help="channel name")
282 parser.add_option("-n", "--nick", default="bot",
283 help="bot nickname")
284 parser.add_option("-w", "--password", default="morereverb",
285 help="channel password")
286 parser.add_option("-q", "--quote", default="/home/brian/irc/zippy.txt",
287 help="Quote file")
288 (options, args) = parser.parse_args()
289
290 commands = ('start', 'stop', 'restart')
291 if len(args) != 1 and args[0] not in commands:
292 parser.print_help()
293 sys.exit("Please provide a single command: start, stop, or restart.")
294
295 config = ConfigParser.ConfigParser()
296 config.read('/home/brian/irc/sg101Bot.ini')
297 db_config = dict(config.items('Database'))
298
299 daemon = BotDaemon('/tmp/sg101Bot.pid', options, db_config)
300 cmd = args[0]
301 if cmd == 'start':
302 daemon.start()
303 elif cmd == 'stop':
304 daemon.stop()
305 elif cmd == 'restart':
306 daemon.restart()