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