]> jfr.im git - erebus.git/blob - bot.py
fix SQL auto-retry
[erebus.git] / bot.py
1 #!/usr/bin/python
2
3 # Erebus IRC bot - Author: John Runyon
4 # "Bot" and "BotConnection" classes (handling a specific "arm")
5
6 import socket, sys, time, threading, os, random
7 from collections import deque
8
9 class MyTimer(threading._Timer):
10 def __init__(self, *args, **kwargs):
11 threading._Timer.__init__(self, *args, **kwargs)
12 self.daemon = True
13
14
15 #bots = {'erebus': bot.Bot(nick='Erebus', user='erebus', bind='', server='irc.quakenet.org', port=6667, realname='Erebus')}
16 class Bot(object):
17 def __init__(self, parent, nick, user, bind, authname, authpass, server, port, realname):
18 self.parent = parent
19 self.nick = nick
20 self.permnick = nick
21 self.user = user
22 self.realname = realname
23
24 self.authname = authname
25 self.authpass = authpass
26
27 curs = self.parent.query("SELECT chname FROM chans WHERE bot = %s AND active = 1", (self.nick,))
28 if curs:
29 chansres = curs.fetchall()
30 curs.close()
31 self.chans = [self.parent.newchannel(self, row['chname']) for row in chansres]
32 else:
33 self.chans = []
34
35 self.conn = BotConnection(self, bind, server, port)
36
37 self.lastreceived = time.time() #time we last received a line from the server
38 self.watchdog()
39
40 self.msgqueue = deque()
41 self.slowmsgqueue = deque()
42 self.makemsgtimer()
43 self.msgtimer.start()
44
45 def __del__(self):
46 try:
47 curs = self.parent.query("UPDATE bots SET connected = 0 WHERE nick = %s", (self.nick,))
48 curs.close()
49 except: pass
50
51 def watchdog(self):
52 if time.time() > self.parent.cfg.get('watchdog', 'maxtime', default=300)+self.lastreceived:
53 self.parse("ERROR :Fake-error from watchdog timer.")
54 self.watchdogtimer = MyTimer(self.parent.cfg.get('watchdog', 'interval', default=30), self.watchdog)
55
56 def log(self, *args, **kwargs):
57 self.parent.log(self.nick, *args, **kwargs)
58
59 def connect(self):
60 if self.conn.connect():
61 self.parent.newfd(self, self.conn.socket.fileno())
62
63 def getdata(self):
64 self.lastreceived = time.time()
65 return self.conn.read()
66
67 def _checknick(self): # check if we're using the right nick, try changing
68 if self.nick != self.permnick and self.conn.registered():
69 self.conn.send("NICK %s" % (self.permnick))
70
71 def parse(self, line):
72 self.log('I', line)
73 pieces = line.split()
74
75 # dispatch dict
76 zero = { #things to look for without source
77 'NOTICE': self._gotconnected,
78 'PING': self._gotping,
79 'ERROR': self._goterror,
80 }
81 one = { #things to look for after source
82 '001': self._got001,
83 '376': self._gotRegistered,
84 '422': self._gotRegistered,
85 'PRIVMSG': self._gotprivmsg,
86 '353': self._got353, #NAMES
87 '354': self._got354, #WHO
88 '433': self._got433, #nick in use
89 'JOIN': self._gotjoin,
90 'PART': self._gotpart,
91 'KICK': self._gotkick,
92 'QUIT': self._gotquit,
93 'NICK': self._gotnick,
94 'MODE': self._gotmode,
95 }
96
97 if self.parent.hasnumhook(pieces[1]):
98 hooks = self.parent.getnumhook(pieces[1])
99 for callback in hooks:
100 try:
101 callback(self, line)
102 except Exception:
103 self.__debug_cbexception("numhook", line)
104
105 if pieces[0] in zero:
106 zero[pieces[0]](pieces)
107 elif pieces[1] in one:
108 one[pieces[1]](pieces)
109
110 def _gotconnected(self, pieces):
111 if not self.conn.registered():
112 self.conn.register()
113 def _gotping(self, pieces):
114 self.conn.send("PONG %s" % (pieces[1]))
115 self._checknick()
116 def _goterror(self, pieces):
117 try:
118 self.quit("Error detected: %s" % ' '.join(pieces))
119 curs = self.parent.query("UPDATE bots SET connected = 0")
120 curs.close()
121 except: pass
122 sys.exit(2)
123 os._exit(2)
124 def _got001(self, pieces):
125 pass # wait until the end of MOTD instead
126 def _gotRegistered(self, pieces):
127 self.conn.registered(True)
128
129 curs = self.parent.query("UPDATE bots SET connected = 1 WHERE nick = %s", (self.nick,))
130 if curs: curs.close()
131
132 self.conn.send("MODE %s +x" % (pieces[2]))
133 if self.authname is not None and self.authpass is not None:
134 self.conn.send("AUTH %s %s" % (self.authname, self.authpass))
135 for c in self.chans:
136 self.join(c.name)
137 def _gotprivmsg(self, pieces):
138 nick = pieces[0].split('!')[0][1:]
139 user = self.parent.user(nick)
140 target = pieces[2]
141 msg = ' '.join(pieces[3:])[1:]
142 self.parsemsg(user, target, msg)
143 def _got353(self, pieces):
144 prefixes = {'@': 'op', '+': 'voice'}
145 chan = self.parent.channel(pieces[4])
146 names = pieces[5:]
147 names[0] = names[0][1:] #remove colon
148 for n in names:
149 if n[0] in prefixes:
150 user = self.parent.user(n[1:])
151 chan.userjoin(user, prefixes[n[0]])
152 else:
153 user = self.parent.user(n)
154 chan.userjoin(user)
155 user.join(chan)
156 def _got354(self, pieces):
157 qt = int(pieces[3])
158 if qt < 3:
159 nick, auth = pieces[4:6]
160 chan = None
161 else:
162 chan, nick, auth = pieces[4:7]
163 chan = self.parent.channel(chan)
164 user = self.parent.user(nick)
165 user.authed(auth)
166
167 if chan is not None:
168 user.join(chan)
169 chan.userjoin(user)
170
171 if qt == 2: # triggered by !auth
172 if user.isauthed():
173 if user.glevel > 0:
174 self.msg(nick, "You are now known as #%s (access level: %s)" % (auth, user.glevel))
175 else:
176 self.msg(nick, "You are now known as #%s (not staff)" % (auth))
177 else:
178 self.msg(nick, "I tried, but you're not authed!")
179 def _got433(self, pieces):
180 if not self.conn.registered(): #we're trying to connect
181 newnick = "%s%d" % (self.nick, random.randint(111,999))
182 self.conn.send("NICK %s" % (newnick))
183 self.nick = newnick
184 def _gotjoin(self, pieces):
185 nick = pieces[0].split('!')[0][1:]
186 chan = self.parent.channel(pieces[2])
187
188 if nick == self.nick:
189 self.conn.send("WHO %s c%%cant,3" % (chan))
190 else:
191 user = self.parent.user(nick, justjoined=True)
192 chan.userjoin(user)
193 user.join(chan)
194 def _clientLeft(self, nick, chan):
195 if nick != self.nick:
196 gone = self.parent.user(nick).part(chan)
197 chan.userpart(self.parent.user(nick))
198 if gone:
199 self.parent.user(nick).quit()
200 del self.parent.users[nick.lower()]
201 def _gotpart(self, pieces):
202 nick = pieces[0].split('!')[0][1:]
203 chan = self.parent.channel(pieces[2])
204 self._clientLeft(nick, chan)
205 def _gotkick(self, pieces):
206 nick = pieces[3]
207 chan = self.parent.channel(pieces[2])
208 self._clientLeft(nick, chan)
209 def _gotquit(self, pieces):
210 nick = pieces[0].split('!')[0][1:]
211 if nick != self.nick:
212 for chan in self.parent.user(nick).chans:
213 chan.userpart(self.parent.user(nick))
214 self.parent.user(nick).quit()
215 del self.parent.users[nick.lower()]
216 def _gotnick(self, pieces):
217 oldnick = pieces[0].split('!')[0][1:]
218 newnick = pieces[2][1:]
219 if newnick.lower() != oldnick.lower():
220 self.parent.users[newnick.lower()] = self.parent.users[oldnick.lower()]
221 del self.parent.users[oldnick.lower()]
222 self.parent.users[newnick.lower()].nickchange(newnick)
223 def _gotmode(self, pieces):
224 source = pieces[0].split('!')[0][1:]
225 chan = pieces[2]
226 if not chan.startswith("#"): return
227 chan = self.parent.channel(pieces[2])
228 mode = pieces[3]
229 args = pieces[4:]
230
231 adding = True
232 for c in mode:
233 if c == '+':
234 adding = True
235 elif c == '-':
236 adding = False
237 elif c == 'o':
238 if adding:
239 chan.userop(self.parent.user(args.pop(0)))
240 else:
241 chan.userdeop(self.parent.user(args.pop(0)))
242 elif c == 'v':
243 if adding:
244 chan.uservoice(self.parent.user(args.pop(0)))
245 else:
246 chan.userdevoice(self.parent.user(args.pop(0)))
247 else:
248 pass # don't care about other modes
249
250 def __debug_cbexception(self, source, *args, **kwargs):
251 if int(self.parent.cfg.get('debug', 'cbexc', default=0)) == 1:
252 self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! CBEXC\1f\ 3 %s" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, source))
253 __import__('traceback').print_exc()
254 self.log('!', "CBEXC %s %r %r" % (source, args, kwargs))
255 # print "%09.3f %s [!] CBEXC %s %r %r" % (time.time() % 100000, self.nick, source, args, kwargs)
256
257
258 def parsemsg(self, user, target, msg):
259 if user.glevel <= -2: return # short circuit if user is IGNORED
260 chan = None
261 chanparam = None # was the channel specified as part of the command?
262 if len(msg) == 0:
263 return
264
265 triggerused = msg.startswith(self.parent.trigger)
266 if triggerused: msg = msg[len(self.parent.trigger):]
267 pieces = msg.split()
268
269 if target == self.nick:
270 if msg.startswith("\001"): #ctcp
271 msg = msg.strip("\001")
272 if msg == "VERSION":
273 self.msg(user, "\001VERSION Erebus v%d.%d - http://github.com/zonidjan/erebus" % (self.parent.APIVERSION, self.parent.RELEASE))
274 return
275
276 if target != self.nick: # message was sent to a channel
277 try:
278 if msg.startswith('*'): # message may be addressed to bot by "*BOTNICK" trigger?
279 if pieces[0][1:].lower() == self.nick.lower():
280 pieces.pop(0) # command actually starts with next word
281 msg = ' '.join(pieces) # command actually starts with next word
282 triggerused = True
283 except IndexError:
284 return # "message" is empty
285
286 if len(pieces) > 1:
287 chanword = pieces[1]
288 if chanword.startswith('#'):
289 chanparam = self.parent.channel(chanword)
290
291 if target != self.nick: # message was sent to a channel
292 chan = self.parent.channel(target)
293 if not triggerused:
294 if self.parent.haschanhook(target.lower()):
295 for callback in self.parent.getchanhook(target.lower()):
296 try:
297 cbret = callback(self, user, chan, *pieces)
298 except NotImplementedError:
299 self.msg(user, "Command not implemented.")
300 except:
301 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
302 self.__debug_cbexception("chanhook", user=user, target=target, msg=msg)
303 return # not to bot, don't process!
304
305 cmd = pieces[0].lower()
306 rancmd = False
307 if self.parent.hashook(cmd):
308 for callback in self.parent.gethook(cmd):
309 if chanparam is not None and (callback.needchan or callback.wantchan):
310 chan = chanparam
311 pieces.pop(1)
312 if chan is None and callback.needchan:
313 rancmd = True
314 self.msg(user, "You need to specify a channel for that command.")
315 elif user.glevel >= callback.reqglevel and (not callback.needchan or chan.levelof(user.auth) >= callback.reqclevel):
316 rancmd = True
317 try:
318 cbret = callback(self, user, chan, target, *pieces[1:])
319 except NotImplementedError:
320 self.msg(user, "Command not implemented.")
321 except Exception:
322 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
323 self.__debug_cbexception("hook", user=user, target=target, msg=msg)
324 except SystemExit as e:
325 try:
326 curs = self.parent.query("UPDATE bots SET connected = 0")
327 curs.close()
328 except: pass
329 raise e
330 else:
331 rancmd = True
332 self.msg(user, "I don't know that command.")
333 if not rancmd:
334 self.msg(user, "You don't have enough access to run that command.")
335
336 def __debug_nomsg(self, target, msg):
337 if int(self.parent.cfg.get('debug', 'nomsg', default=0)) == 1:
338 self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! NOMSG\1f\ 3 %r, %r" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, target, msg))
339 self.log('!', "!!! NOMSG")
340 # print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
341 __import__('traceback').print_stack()
342
343 def msg(self, target, msg):
344 if self.parent.cfg.getboolean('erebus', 'nofakelag'): return self.fastmsg(target, msg)
345 cmd = self._formatmsg(target, msg)
346 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
347 self.msgqueue.append(cmd)
348 else:
349 self.conn.send(cmd)
350 self.conn.exceeded = True
351
352 def slowmsg(self, target, msg):
353 if self.parent.cfg.getboolean('erebus', 'nofakelag'): return self.fastmsg(target, msg)
354 cmd = self._formatmsg(target, msg)
355 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
356 self.slowmsgqueue.append(cmd)
357 else:
358 self.conn.send(cmd)
359 self.conn.exceeded = True
360
361 def fastmsg(self, target, msg):
362 self.conn.send(self._formatmsg(target, msg))
363 self.conn.exceeded = True
364
365 def _formatmsg(self, target, msg):
366 if target is None or msg is None:
367 return self.__debug_nomsg(target, msg)
368
369 target = str(target)
370
371 if target.startswith('#'): command = "PRIVMSG %s :%s" % (target, msg)
372 else: command = "NOTICE %s :%s" % (target, msg)
373
374 return command
375
376 def _popmsg(self):
377 self.makemsgtimer()
378 self.conn.bytessent -= self.conn.recvq/3
379 if self.conn.bytessent < 0: self.conn.bytessent = 0
380 self.conn.exceeded = False
381
382 try:
383 cmd = self.msgqueue.popleft()
384 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
385 self.conn.send(cmd)
386 self.conn.exceeded = True
387 else: raise IndexError
388 except IndexError:
389 try:
390 cmd = self.slowmsgqueue.popleft()
391 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
392 self.conn.send(cmd)
393 self.conn.exceeded = True
394 except IndexError:
395 pass
396 self.msgtimer.start()
397
398 def makemsgtimer(self):
399 self.msgtimer = threading.Timer(3, self._popmsg)
400 self.msgtimer.daemon = True
401
402 def join(self, chan):
403 self.conn.send("JOIN %s" % (chan))
404
405 def part(self, chan):
406 self.conn.send("PART %s" % (chan))
407
408 def quit(self, reason="Shutdown"):
409 self.conn.send("QUIT :%s" % (reason))
410
411 def __str__(self): return self.nick
412 def __repr__(self): return "<Bot %r>" % (self.nick)
413
414 class BotConnection(object):
415 def __init__(self, parent, bind, server, port):
416 self.parent = parent
417 self.buffer = ''
418 self.socket = None
419
420 self.bind = bind
421 self.server = server
422 self.port = int(port)
423
424 self.state = 0 # 0=disconnected, 1=registering, 2=connected
425
426 self.bytessent = 0
427 self.recvq = 500
428 self.exceeded = False
429
430 def connect(self):
431 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
432 self.socket.bind((self.bind, 0))
433 self.socket.connect((self.server, self.port))
434 return True
435 def register(self):
436 if self.state == 0:
437 self.send("NICK %s" % (self.parent.nick))
438 self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
439 self.state = 1
440 return True
441
442 def registered(self, done=False):
443 if done: self.state = 2
444 return self.state == 2
445
446 def send(self, line):
447 self.parent.log('O', line)
448 # print "%09.3f %s [O] %s" % (time.time() % 100000, self.parent.nick, line)
449 self.bytessent += len(line)
450 self._write(line)
451
452 def _write(self, line):
453 self.socket.sendall(line+"\r\n")
454
455 def read(self):
456 self.buffer += self.socket.recv(8192)
457 lines = []
458
459 while "\r\n" in self.buffer:
460 pieces = self.buffer.split("\r\n", 1)
461 # self.parent.log('I', pieces[0]) # replaced by statement in Bot.parse()
462 # print "%09.3f %s [I] %s" % (time.time() % 100000, self.parent.nick, pieces[0])
463 lines.append(pieces[0])
464 self.buffer = pieces[1]
465
466 return lines
467
468 def __str__(self): return self.parent.nick
469 def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)