]> jfr.im git - erebus.git/blob - bot.py
add config option nofakelag
[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.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 self.watchdogtimer = MyTimer(self.parent.cfg.get('watchdog', 'interval', default=30), self.watchdog)
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 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 curs = self.parent.db.cursor()
326 curs.execute("UPDATE bots SET connected = 0")
327 curs.close()
328 raise e
329 else:
330 rancmd = True
331 self.msg(user, "I don't know that command.")
332 if not rancmd:
333 self.msg(user, "You don't have enough access to run that command.")
334
335 def __debug_nomsg(self, target, msg):
336 if int(self.parent.cfg.get('debug', 'nomsg', default=0)) == 1:
337 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))
338 self.log('!', "!!! NOMSG")
339 # print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
340 __import__('traceback').print_stack()
341
342 def msg(self, target, msg):
343 if self.parent.cfg.getboolean('erebus', 'nofakelag'): return self.fastmsg(target, msg)
344 cmd = self._formatmsg(target, msg)
345 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
346 self.msgqueue.append(cmd)
347 else:
348 self.conn.send(cmd)
349 self.conn.exceeded = True
350
351 def slowmsg(self, target, msg):
352 if self.parent.cfg.getboolean('erebus', 'nofakelag'): return self.fastmsg(target, msg)
353 cmd = self._formatmsg(target, msg)
354 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
355 self.slowmsgqueue.append(cmd)
356 else:
357 self.conn.send(cmd)
358 self.conn.exceeded = True
359
360 def fastmsg(self, target, msg):
361 self.conn.send(self._formatmsg(target, msg))
362 self.conn.exceeded = True
363
364 def _formatmsg(self, target, msg):
365 if target is None or msg is None:
366 return self.__debug_nomsg(target, msg)
367
368 target = str(target)
369
370 if target.startswith('#'): command = "PRIVMSG %s :%s" % (target, msg)
371 else: command = "NOTICE %s :%s" % (target, msg)
372
373 return command
374
375 def _popmsg(self):
376 self.makemsgtimer()
377 self.conn.bytessent -= self.conn.recvq/3
378 if self.conn.bytessent < 0: self.conn.bytessent = 0
379 self.conn.exceeded = False
380
381 try:
382 cmd = self.msgqueue.popleft()
383 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
384 self.conn.send(cmd)
385 self.conn.exceeded = True
386 else: raise IndexError
387 except IndexError:
388 try:
389 cmd = self.slowmsgqueue.popleft()
390 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
391 self.conn.send(cmd)
392 self.conn.exceeded = True
393 except IndexError:
394 pass
395 self.msgtimer.start()
396
397 def makemsgtimer(self):
398 self.msgtimer = threading.Timer(3, self._popmsg)
399 self.msgtimer.daemon = True
400
401 def join(self, chan):
402 self.conn.send("JOIN %s" % (chan))
403
404 def part(self, chan):
405 self.conn.send("PART %s" % (chan))
406
407 def quit(self, reason="Shutdown"):
408 self.conn.send("QUIT :%s" % (reason))
409
410 def __str__(self): return self.nick
411 def __repr__(self): return "<Bot %r>" % (self.nick)
412
413 class BotConnection(object):
414 def __init__(self, parent, bind, server, port):
415 self.parent = parent
416 self.buffer = ''
417 self.socket = None
418
419 self.bind = bind
420 self.server = server
421 self.port = int(port)
422
423 self.state = 0 # 0=disconnected, 1=registering, 2=connected
424
425 self.bytessent = 0
426 self.recvq = 500
427 self.exceeded = False
428
429 def connect(self):
430 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
431 self.socket.bind((self.bind, 0))
432 self.socket.connect((self.server, self.port))
433 return True
434 def register(self):
435 if self.state == 0:
436 self.send("NICK %s" % (self.parent.nick))
437 self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
438 self.state = 1
439 return True
440
441 def registered(self, done=False):
442 if done: self.state = 2
443 return self.state == 2
444
445 def send(self, line):
446 self.parent.log('O', line)
447 # print "%09.3f %s [O] %s" % (time.time() % 100000, self.parent.nick, line)
448 self.bytessent += len(line)
449 self._write(line)
450
451 def _write(self, line):
452 self.socket.sendall(line+"\r\n")
453
454 def read(self):
455 self.buffer += self.socket.recv(8192)
456 lines = []
457
458 while "\r\n" in self.buffer:
459 pieces = self.buffer.split("\r\n", 1)
460 # self.parent.log('I', pieces[0]) # replaced by statement in Bot.parse()
461 # print "%09.3f %s [I] %s" % (time.time() % 100000, self.parent.nick, pieces[0])
462 lines.append(pieces[0])
463 self.buffer = pieces[1]
464
465 return lines
466
467 def __str__(self): return self.parent.nick
468 def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)