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