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