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