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