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