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