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