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