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