Mercurial > public > sg101
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() |