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