]> jfr.im git - erebus.git/blob - bot.py
add watchdog timer. fixes #12
[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.db.cursor()
28 if curs.execute("SELECT chname FROM chans WHERE bot = %s AND active = 1", (self.nick,)):
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.watchdogtimer = MyTimer(self.parent.cfg.get('watchdog', 'interval', default=30), self.watchdog)
39
40 self.msgqueue = deque()
41 self.slowmsgqueue = deque()
42 self.makemsgtimer()
43 self.msgtimer.start()
44
45 def __del__(self):
46 curs = self.parent.db.cursor()
47 curs.execute("UPDATE bots SET connected = 0 WHERE nick = %s", (self.nick,))
48 curs.close()
49
50 def watchdog(self):
51 if time.time() > self.parent.cfg.get('watchdog', 'maxtime', default=300)+self.lastreceived:
52 self.parse("ERROR :Fake-error from watchdog timer.")
53
54
55 def log(self, *args, **kwargs):
56 self.parent.log(self.nick, *args, **kwargs)
57
58 def connect(self):
59 if self.conn.connect():
60 self.parent.newfd(self, self.conn.socket.fileno())
61
62 def getdata(self):
63 self.lastreceived = time.time()
64 return self.conn.read()
65
66 def _checknick(self): # check if we're using the right nick, try changing
67 if self.nick != self.permnick and self.conn.registered():
68 self.conn.send("NICK %s" % (self.permnick))
69
70 def parse(self, line):
71 self.log('I', line)
72 pieces = line.split()
73
74 # dispatch dict
75 zero = { #things to look for without source
76 'NOTICE': self._gotconnected,
77 'PING': self._gotping,
78 'ERROR': self._goterror,
79 }
80 one = { #things to look for after source
81 '001': self._got001,
82 '376': self._gotRegistered,
83 '422': self._gotRegistered,
84 'PRIVMSG': self._gotprivmsg,
85 '353': self._got353, #NAMES
86 '354': self._got354, #WHO
87 '433': self._got433, #nick in use
88 'JOIN': self._gotjoin,
89 'PART': self._gotpart,
90 'KICK': self._gotkick,
91 'QUIT': self._gotquit,
92 'NICK': self._gotnick,
93 'MODE': self._gotmode,
94 }
95
96 if self.parent.hasnumhook(pieces[1]):
97 hooks = self.parent.getnumhook(pieces[1])
98 for callback in hooks:
99 try:
100 callback(self, line)
101 except Exception:
102 self.__debug_cbexception("numhook", line)
103
104 if pieces[0] in zero:
105 zero[pieces[0]](pieces)
106 elif pieces[1] in one:
107 one[pieces[1]](pieces)
108
109 def _gotconnected(self, pieces):
110 if not self.conn.registered():
111 self.conn.register()
112 def _gotping(self, pieces):
113 self.conn.send("PONG %s" % (pieces[1]))
114 self._checknick()
115 def _goterror(self, pieces):
116 try: self.quit("Error detected: %s" % ' '.join(pieces))
117 except: pass
118 curs = self.parent.db.cursor()
119 curs.execute("UPDATE bots SET connected = 0")
120 curs.close()
121 sys.exit(2)
122 os._exit(2)
123 def _got001(self, pieces):
124 pass # wait until the end of MOTD instead
125 def _gotRegistered(self, pieces):
126 self.conn.registered(True)
127
128 curs = self.parent.db.cursor()
129 curs.execute("UPDATE bots SET connected = 1 WHERE nick = %s", (self.nick,))
130 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 chan = self.parent.channel(pieces[4])
145 names = pieces[5:]
146 names[0] = names[0][1:] #remove colon
147 for n in names:
148 user = self.parent.user(n.lstrip('@+'))
149 if n[0] == '@':
150 chan.userjoin(user, 'op')
151 elif n[0] == '+':
152 chan.userjoin(user, 'voice')
153 else:
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 = self.parent.channel(pieces[2])
226 mode = pieces[3]
227 args = pieces[4:]
228
229 adding = True
230 for c in mode:
231 if c == '+':
232 adding = True
233 elif c == '-':
234 adding = False
235 elif c == 'o':
236 if adding:
237 chan.userop(self.parent.user(args.pop(0)))
238 else:
239 chan.userdeop(self.parent.user(args.pop(0)))
240 elif c == 'v':
241 if adding:
242 chan.uservoice(self.parent.user(args.pop(0)))
243 else:
244 chan.userdevoice(self.parent.user(args.pop(0)))
245 else:
246 pass # don't care about other modes
247
248 def __debug_cbexception(self, source, *args, **kwargs):
249 if int(self.parent.cfg.get('debug', 'cbexc', default=0)) == 1:
250 self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! CBEXC\1f\ 3 %s" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, source))
251 __import__('traceback').print_exc()
252 self.log('!', "CBEXC %s %r %r" % (source, args, kwargs))
253 # print "%09.3f %s [!] CBEXC %s %r %r" % (time.time() % 100000, self.nick, source, args, kwargs)
254
255
256 def parsemsg(self, user, target, msg):
257 if user.glevel <= -2: return # short circuit if user is IGNORED
258 chan = None
259 chanparam = None # was the channel specified as part of the command?
260 if len(msg) == 0:
261 return
262
263 triggerused = msg[0] == self.parent.trigger
264 if triggerused: msg = msg[1:]
265 pieces = msg.split()
266
267 if target == self.nick:
268 if msg[0] == "\001": #ctcp
269 msg = msg.strip("\001")
270 if msg == "VERSION":
271 self.msg(user, "\001VERSION Erebus v%d.%d - http://github.com/zonidjan/erebus" % (self.parent.APIVERSION, self.parent.RELEASE))
272 return
273 if len(pieces) > 1:
274 chanword = pieces[1]
275 if chanword[0] == '#':
276 chanparam = self.parent.channel(chanword)
277
278 if target != self.nick: # message was sent to a channel
279 chan = self.parent.channel(target)
280 try:
281 if msg[0] == '*': # message may be addressed to bot by "*BOTNICK" trigger?
282 if pieces[0][1:].lower() == self.nick.lower():
283 pieces.pop(0) # command actually starts with next word
284 msg = ' '.join(pieces) # command actually starts with next word
285 elif not triggerused:
286 if self.parent.haschanhook(target.lower()):
287 for callback in self.parent.getchanhook(target.lower()):
288 try:
289 cbret = callback(self, user, chan, *pieces)
290 except NotImplementedError:
291 self.msg(user, "Command not implemented.")
292 except:
293 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
294 self.__debug_cbexception("chanhook", user=user, target=target, msg=msg)
295 return # not to bot, don't process!
296 except IndexError:
297 return # "message" is empty
298
299 cmd = pieces[0].lower()
300 rancmd = False
301 if self.parent.hashook(cmd):
302 for callback in self.parent.gethook(cmd):
303 if chanparam is not None and callback.needchan:
304 chan = chanparam
305 pieces.pop(1)
306 if chan is None and callback.needchan:
307 rancmd = True
308 self.msg(user, "You need to specify a channel for that command.")
309 elif user.glevel >= callback.reqglevel and (not callback.needchan or chan.levelof(user.auth) >= callback.reqclevel):
310 rancmd = True
311 try:
312 cbret = callback(self, user, chan, target, *pieces[1:])
313 except NotImplementedError:
314 self.msg(user, "Command not implemented.")
315 except Exception:
316 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
317 self.__debug_cbexception("hook", user=user, target=target, msg=msg)
318 except SystemExit as e:
319 curs = self.parent.db.cursor()
320 curs.execute("UPDATE bots SET connected = 0")
321 curs.close()
322 raise e
323 else:
324 rancmd = True
325 self.msg(user, "I don't know that command.")
326 if not rancmd:
327 self.msg(user, "You don't have enough access to run that command.")
328
329 def __debug_nomsg(self, target, msg):
330 if int(self.parent.cfg.get('debug', 'nomsg', default=0)) == 1:
331 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))
332 self.log('!', "!!! NOMSG")
333 # print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
334 __import__('traceback').print_stack()
335
336 def msg(self, target, msg):
337 cmd = self._formatmsg(target, msg)
338 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
339 self.msgqueue.append(cmd)
340 else:
341 self.conn.send(cmd)
342 self.conn.exceeded = True
343
344 def slowmsg(self, target, msg):
345 cmd = self._formatmsg(target, msg)
346 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
347 self.slowmsgqueue.append(cmd)
348 else:
349 self.conn.send(cmd)
350 self.conn.exceeded = True
351
352 def fastmsg(self, target, msg):
353 self.conn.send(self._formatmsg(target, msg))
354 self.conn.exceeded = True
355
356 def _formatmsg(self, target, msg):
357 if target is None or msg is None:
358 return self.__debug_nomsg(target, msg)
359
360 target = str(target)
361
362 if target[0] == '#': command = "PRIVMSG %s :%s" % (target, msg)
363 else: command = "NOTICE %s :%s" % (target, msg)
364
365 return command
366
367 def _popmsg(self):
368 self.makemsgtimer()
369 self.conn.bytessent -= self.conn.recvq/3
370 if self.conn.bytessent < 0: self.conn.bytessent = 0
371 self.conn.exceeded = False
372
373 try:
374 cmd = self.msgqueue.popleft()
375 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
376 self.conn.send(cmd)
377 self.conn.exceeded = True
378 else: raise IndexError
379 except IndexError:
380 try:
381 cmd = self.slowmsgqueue.popleft()
382 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
383 self.conn.send(cmd)
384 self.conn.exceeded = True
385 except IndexError:
386 pass
387 self.msgtimer.start()
388
389 def makemsgtimer(self):
390 self.msgtimer = threading.Timer(3, self._popmsg)
391 self.msgtimer.daemon = True
392
393 def join(self, chan):
394 self.conn.send("JOIN %s" % (chan))
395
396 def part(self, chan):
397 self.conn.send("PART %s" % (chan))
398
399 def quit(self, reason="Shutdown"):
400 self.conn.send("QUIT :%s" % (reason))
401
402 def __str__(self): return self.nick
403 def __repr__(self): return "<Bot %r>" % (self.nick)
404
405 class BotConnection(object):
406 def __init__(self, parent, bind, server, port):
407 self.parent = parent
408 self.buffer = ''
409 self.socket = None
410
411 self.bind = bind
412 self.server = server
413 self.port = int(port)
414
415 self.state = 0 # 0=disconnected, 1=registering, 2=connected
416
417 self.bytessent = 0
418 self.recvq = 500
419 self.exceeded = False
420
421 def connect(self):
422 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
423 self.socket.bind((self.bind, 0))
424 self.socket.connect((self.server, self.port))
425 return True
426 def register(self):
427 if self.state == 0:
428 self.send("NICK %s" % (self.parent.nick))
429 self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
430 self.state = 1
431 return True
432
433 def registered(self, done=False):
434 if done: self.state = 2
435 return self.state == 2
436
437 def send(self, line):
438 self.parent.log('O', line)
439 # print "%09.3f %s [O] %s" % (time.time() % 100000, self.parent.nick, line)
440 self.bytessent += len(line)
441 self._write(line)
442
443 def _write(self, line):
444 self.socket.sendall(line+"\r\n")
445
446 def read(self):
447 self.buffer += self.socket.recv(8192)
448 lines = []
449
450 while "\r\n" in self.buffer:
451 pieces = self.buffer.split("\r\n", 1)
452 # self.parent.log('I', pieces[0]) # replaced by statement in Bot.parse()
453 # print "%09.3f %s [I] %s" % (time.time() % 100000, self.parent.nick, pieces[0])
454 lines.append(pieces[0])
455 self.buffer = pieces[1]
456
457 return lines
458
459 def __str__(self): return self.parent.nick
460 def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)