]> jfr.im git - z_archive/Ophion.git/blob - bot.py
Cache.hookcmd() prototype changes
[z_archive/Ophion.git] / bot.py
1 #!/usr/bin/python
2
3 ###
4 ### Ophion Bot
5 ### v1.00
6 ### Copyright 2012 John Runyon
7 ### <https://github.com/Mustis/Ophion>
8 ###
9
10 ## CONFIG
11 dbhost = 'localhost'
12 dbuser = 'bot'
13 dbpass = 'roboticism'
14 dbname = 'bot'
15 rootdir = '/home/bots/'
16 logfile = rootdir+'/output.log'
17 excfile = rootdir+'/exception.log'
18
19 oidfile = '/home/bots/.oidentd.conf'
20 identprefix = 'ophion' # ident will be <identprefix><bot ID#>
21
22 import socket, select, sys, os, signal, time
23 from threading import *
24 from traceback import print_exc
25
26 import MySQLdb, MySQLdb.cursors
27
28 from classes import *
29 from util import *
30
31 cache = Cache()
32
33 def quit(reason, restart=False):
34 cache.quitting = True
35 cache.mainBot.cmsg('fatal', "Shutting down because: %s" % (reason))
36 time.sleep(1)
37 for bot in cache.bots.values():
38 bot.disconnect()
39 for tkey in cache.timers.keys():
40 cache.timers[tkey].cancel()
41 del cache.timers[tkey]
42 for modf in cache.modules:
43 unloadmod(modf, cache, True)
44
45 if restart:
46 os.execv(sys.argv[0], sys.argv)
47 sys.exit(0)
48 def sigHandle(signum, stack):
49 quit("Signal: %d" % (signum))
50 def online(bid):
51 return (bid in cache.bots and cache.bots[bid].online)
52 def toofew(bot, nick): # like: if len(params) < NEEDED: return toofew(bot, nick)
53 bot.msg(nick, "Too few parameters for that command.")
54 return False
55
56 def sendRaws():
57 for bot in cache.bots.values():
58 bot.sendRaws()
59 if not cache.quitting:
60 t = Timer(1, sendRaws)
61 t.daemon = True
62 if 'raws' in cache.timers: del cache.timers['raws']
63 cache.timers['raws'] = t
64 t.start()
65
66 def connectBots():
67 curs = cache.dbc.cursor()
68 curs.execute("SELECT 1")
69 curs.close()
70
71 connected = 0
72 for bot in cache.bots.values():
73 if not bot.online:
74 ident = "%s%d" % (identprefix, bot.id)
75 oid = open(oidfile, 'w')
76 oid.write("global {\n\treply \"%s\"\n}" % ident)
77 oid.close()
78 if bot.connect(ident):
79 bot.cmsg('info', "Connected")
80 connected += 1
81 if connected == 2:
82 break
83
84 if not cache.quitting:
85 if connected < 2: interval = 300
86 else: interval = 60
87 t = Timer(interval, connectBots)
88 t.daemon = True
89 if 'conn' in cache.timers: del cache.timers['conn']
90 cache.timers['conn'] = t
91 t.start()
92
93 return connected
94 def makeBots():
95 curs = cache.dbc.cursor()
96 curs.execute("SELECT id FROM bots")
97 row = curs.fetchone()
98 while row is not None:
99 bot = Bot(row['id'])
100 if row['id'] == 0:
101 cache.mainBot = bot
102 bot.mainbot = True
103 cache.bots[row['id']] = bot
104 row = curs.fetchone()
105
106 def fillCache():
107 curs = cache.dbc.cursor()
108 curs.execute("SELECT username, level FROM admins")
109 row = curs.fetchone()
110 while row is not None:
111 cache.admins[row['username']] = row['level']
112 row = curs.fetchone()
113 curs.execute("SELECT chname FROM chans ORDER BY id LIMIT 2")
114 rows = curs.fetchall()
115 cache.ctrl = rows[0]['chname']
116 cache.home = rows[1]['chname']
117 curs.close()
118 makeBots()
119
120 def parseCmd(bot, line, linepieces, msg, iscm):
121 hostmask = linepieces[0][1:]
122 fromnick = hostmask.split('!')[0]
123 target = linepieces[2]
124 pieces = msg.split()
125 cmd = pieces[0].upper()
126 if len(pieces) == 1: params = ''
127 else: params = ' '.join(pieces[1:])
128
129 ret = ""
130
131 if cmd not in cache.cmds:
132 noaccess(bot, fromnick)
133 return ret
134 else: ci = cache.cmds[cmd]
135
136 if not ci['isadmin']:
137 if fromnick not in cache.users:
138 cache.users[fromnick] = User(fromnick)
139 auth = cache.users[fromnick].auth
140 if not iscm:
141 if ci['reqchan']:
142 target = pieces.pop(1).lower()
143 params = ' '.join(pieces[1:])
144 else:
145 target = fromnick
146 if target not in cache.chans:
147 bot.msg(fromnick, "No such channel.")
148 return ret
149 chan = cache.chans[target]
150 if auth not in chan.alist or chan.alist[auth] < ci['level']:
151 noaccess(bot, fromnick, True)
152 return ret
153 elif len(pieces)-1 < cache.cmds[cmd]['params']:
154 toofew(bot, fromnick)
155 return ret
156 else:
157 try: ret = cache.cmds[cmd]['func'](fromnick, target, params, chan.bot, cache)
158 except Exception as e:
159 print_exc(None, cache.excfile)
160 bot.msg(fromnick, "An error occurred, sorry! Try again later.")
161 cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line))
162
163 else: # admin/global command
164 if fromnick in cache.users: glblevel = cache.users[fromnick].access
165 else: glblevel = 0
166 if glblevel < ci['level']:
167 noaccess(bot, fromnick)
168 elif len(pieces)-1 < cache.cmds[cmd]['params']:
169 toofew(bot, fromnick)
170 else:
171 try: ret = cache.cmds[cmd]['func'](fromnick, target, params, bot, cache)
172 except Exception as e:
173 print_exc(None, cache.excfile)
174 bot.msg(fromnick, "An error occurred, sorry! Try again later.")
175 cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line))
176 return ret
177
178 def _parse(bot, line):
179 pieces = line.split()
180 if pieces[1] == "PRIVMSG":
181 target = pieces[2]
182 msg = ' '.join(pieces[3:]).lstrip(':')
183 if msg[0] == cache.trigger:
184 return parseCmd(bot, line, pieces, msg[1:], True)
185 elif target == bot.nick:
186 return parseCmd(bot, line, pieces, msg, False)
187 elif pieces[1] == "JOIN":
188 nick = pieces[0][1:].split('!')[0]
189 host = pieces[0][1:].split('@')[1]
190 chname = pieces[2].lower()
191 if chname in cache.chans:
192 bot.joined(chname, nick)
193 elif pieces[1] == "PART":
194 nick = pieces[0][1:].split('!')[0]
195 chname = pieces[2].lower()
196 if chname in cache.chans:
197 bot.parted(chname, nick)
198 elif pieces[1] == "QUIT":
199 nick = pieces[0][1:].split('!')[0]
200 bot.quit(nick)
201 elif pieces[1] == "NICK":
202 fromnick = pieces[0][1:].split('!')[0]
203 tonick = pieces[2][1:]
204 cache.users[tonick] = cache.users[fromnick]
205 del cache.users[fromnick]
206 cache.users[tonick].nick = tonick
207 elif pieces[0] == "PING":
208 bot.rawnow("PONG %s" % (pieces[1]))
209 elif pieces[0] == "ERROR":
210 try: bot.disconnect()
211 except: pass
212 elif pieces[1] in cache.nums:
213 for fn in cache.nums[pieces[1]]:
214 try: fn(line, bot)
215 except Exception as e:
216 print_exc(None, cache.excfile)
217 bot.msg(fromnick, "An error occurred, sorry! Try again later.")
218 cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line))
219
220 def parse(bot):
221 line = bot.get().strip()
222 return _parse(bot, line)
223
224
225 def loop():
226 while True:
227 ready = cache.poll.poll()
228 for fde in ready:
229 ret = ""
230 if fde[0] == sys.stdin.fileno():
231 line = sys.stdin.readline().strip()
232 pieces = line.split(None, 2)
233 if len(pieces) == 3 and pieces[1].isdigit():
234 mode = pieces[0]
235 bid = int(pieces[1])
236 line = pieces[2]
237 if mode.upper() == "IN": ret = _parse(cache.bots[bid], line)
238 elif mode.upper() == "OUT": cache.bots[bid].rawnow(line)
239 else: print "ERROR! <'IN'|'OUT'>:<bid>:<line>"
240 else: print "ERROR! <'IN'|'OUT'>:<bid>:<line>"
241 elif fde[0] in cache.botsByFD: # it's a bot
242 bot = cache.botsByFD[fde[0]]
243 if fde[1] & select.POLLERR or fde[1] & select.POLLHUP or fde[1] & select.POLLNVAL:
244 bot.disconnect()
245 else:
246 ret = parse(bot)
247 if ret == "QUIT": break
248 else: continue
249 break # if the "for" was broken, break the while as well.
250
251
252 signal.signal(signal.SIGHUP, signal.SIG_IGN)
253 signal.signal(signal.SIGINT, sigHandle)
254
255 cache.excfile = open(excfile, 'a')
256
257 sys.path.append(rootdir+'/modules')
258 sys.path.append(rootdir+'/modules/autoload')
259 for modf in os.listdir(rootdir+'/modules/autoload'):
260 if modf[-3:] == ".py":
261 loadmod(modf[:-3], cache)
262
263 cache.dbc = MySQLdb.connect(host=dbhost, user=dbuser, passwd=dbpass, db=dbname, cursorclass=MySQLdb.cursors.DictCursor)
264 fillCache()
265
266 cache.poll = select.poll()
267
268 cache.poll.register(sys.stdin.fileno(), select.POLLIN)
269
270 t = Timer(1, sendRaws)
271 t.daemon = True
272 cache.timers['raws'] = t
273 t.start()
274
275 connectBots()
276 t = Timer(60, connectBots)
277 t.daemon = True
278 cache.timers['conn'] = t
279 t.start()
280
281 loop()