]> jfr.im git - erebus.git/blob - bot.py
add wantchan - fixes #13 - will consume channel if available, but not require it
[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 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 = 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.startswith(self.parent.trigger)
264 if triggerused: msg = msg[len(self.parent.trigger):]
265 pieces = msg.split()
266
267 if target == self.nick:
268 if msg.startswith("\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
274 if target != self.nick: # message was sent to a channel
275 try:
276 if msg.startswith('*'): # message may be addressed to bot by "*BOTNICK" trigger?
277 if pieces[0][1:].lower() == self.nick.lower():
278 pieces.pop(0) # command actually starts with next word
279 msg = ' '.join(pieces) # command actually starts with next word
280 triggerused = True
281 except IndexError:
282 return # "message" is empty
283
284 if len(pieces) > 1:
285 chanword = pieces[1]
286 if chanword.startswith('#'):
287 chanparam = self.parent.channel(chanword)
288
289 if target != self.nick: # message was sent to a channel
290 chan = self.parent.channel(target)
291 if not triggerused:
292 if self.parent.haschanhook(target.lower()):
293 for callback in self.parent.getchanhook(target.lower()):
294 try:
295 cbret = callback(self, user, chan, *pieces)
296 except NotImplementedError:
297 self.msg(user, "Command not implemented.")
298 except:
299 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
300 self.__debug_cbexception("chanhook", user=user, target=target, msg=msg)
301 return # not to bot, don't process!
302
303 cmd = pieces[0].lower()
304 rancmd = False
305 if self.parent.hashook(cmd):
306 for callback in self.parent.gethook(cmd):
307 if chanparam is not None and (callback.needchan or callback.wantchan):
308 chan = chanparam
309 pieces.pop(1)
310 if chan is None and callback.needchan:
311 rancmd = True
312 self.msg(user, "You need to specify a channel for that command.")
313 elif user.glevel >= callback.reqglevel and (not callback.needchan or chan.levelof(user.auth) >= callback.reqclevel):
314 rancmd = True
315 try:
316 cbret = callback(self, user, chan, target, *pieces[1:])
317 except NotImplementedError:
318 self.msg(user, "Command not implemented.")
319 except Exception:
320 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
321 self.__debug_cbexception("hook", user=user, target=target, msg=msg)
322 except SystemExit as e:
323 curs = self.parent.db.cursor()
324 curs.execute("UPDATE bots SET connected = 0")
325 curs.close()
326 raise e
327 else:
328 rancmd = True
329 self.msg(user, "I don't know that command.")
330 if not rancmd:
331 self.msg(user, "You don't have enough access to run that command.")
332
333 def __debug_nomsg(self, target, msg):
334 if int(self.parent.cfg.get('debug', 'nomsg', default=0)) == 1:
335 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))
336 self.log('!', "!!! NOMSG")
337 # print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
338 __import__('traceback').print_stack()
339
340 def msg(self, target, msg):
341 cmd = self._formatmsg(target, msg)
342 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
343 self.msgqueue.append(cmd)
344 else:
345 self.conn.send(cmd)
346 self.conn.exceeded = True
347
348 def slowmsg(self, target, msg):
349 cmd = self._formatmsg(target, msg)
350 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
351 self.slowmsgqueue.append(cmd)
352 else:
353 self.conn.send(cmd)
354 self.conn.exceeded = True
355
356 def fastmsg(self, target, msg):
357 self.conn.send(self._formatmsg(target, msg))
358 self.conn.exceeded = True
359
360 def _formatmsg(self, target, msg):
361 if target is None or msg is None:
362 return self.__debug_nomsg(target, msg)
363
364 target = str(target)
365
366 if target.startswith('#'): command = "PRIVMSG %s :%s" % (target, msg)
367 else: command = "NOTICE %s :%s" % (target, msg)
368
369 return command
370
371 def _popmsg(self):
372 self.makemsgtimer()
373 self.conn.bytessent -= self.conn.recvq/3
374 if self.conn.bytessent < 0: self.conn.bytessent = 0
375 self.conn.exceeded = False
376
377 try:
378 cmd = self.msgqueue.popleft()
379 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
380 self.conn.send(cmd)
381 self.conn.exceeded = True
382 else: raise IndexError
383 except IndexError:
384 try:
385 cmd = self.slowmsgqueue.popleft()
386 if not self.conn.exceeded and self.conn.bytessent+len(cmd) < self.conn.recvq:
387 self.conn.send(cmd)
388 self.conn.exceeded = True
389 except IndexError:
390 pass
391 self.msgtimer.start()
392
393 def makemsgtimer(self):
394 self.msgtimer = threading.Timer(3, self._popmsg)
395 self.msgtimer.daemon = True
396
397 def join(self, chan):
398 self.conn.send("JOIN %s" % (chan))
399
400 def part(self, chan):
401 self.conn.send("PART %s" % (chan))
402
403 def quit(self, reason="Shutdown"):
404 self.conn.send("QUIT :%s" % (reason))
405
406 def __str__(self): return self.nick
407 def __repr__(self): return "<Bot %r>" % (self.nick)
408
409 class BotConnection(object):
410 def __init__(self, parent, bind, server, port):
411 self.parent = parent
412 self.buffer = ''
413 self.socket = None
414
415 self.bind = bind
416 self.server = server
417 self.port = int(port)
418
419 self.state = 0 # 0=disconnected, 1=registering, 2=connected
420
421 self.bytessent = 0
422 self.recvq = 500
423 self.exceeded = False
424
425 def connect(self):
426 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
427 self.socket.bind((self.bind, 0))
428 self.socket.connect((self.server, self.port))
429 return True
430 def register(self):
431 if self.state == 0:
432 self.send("NICK %s" % (self.parent.nick))
433 self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
434 self.state = 1
435 return True
436
437 def registered(self, done=False):
438 if done: self.state = 2
439 return self.state == 2
440
441 def send(self, line):
442 self.parent.log('O', line)
443 # print "%09.3f %s [O] %s" % (time.time() % 100000, self.parent.nick, line)
444 self.bytessent += len(line)
445 self._write(line)
446
447 def _write(self, line):
448 self.socket.sendall(line+"\r\n")
449
450 def read(self):
451 self.buffer += self.socket.recv(8192)
452 lines = []
453
454 while "\r\n" in self.buffer:
455 pieces = self.buffer.split("\r\n", 1)
456 # self.parent.log('I', pieces[0]) # replaced by statement in Bot.parse()
457 # print "%09.3f %s [I] %s" % (time.time() % 100000, self.parent.nick, pieces[0])
458 lines.append(pieces[0])
459 self.buffer = pieces[1]
460
461 return lines
462
463 def __str__(self): return self.parent.nick
464 def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)