Skip to content
Newer
Older
100644 571 lines (505 sloc) 19 KB
5a7b0a1 @richo Merge branch 'universal_exceptions'
authored
1 from lib import *
4ef1e83 @richo Fix borked newlines
authored
2 import threading
3 import socket
4 import sys
5 import time
6 import ssl
7 import re
8 # Rebuild module tree
9 import regen_modules
10 regen_modules.rebuild_bModules()
11 import bModules
12 import config
13 import signal
14 import auth
15 import logging
16
17
18 def should_reconnect():
19 """This hook lies in here because it'll give the rest of the structure a fairly central place
20 to pull strings from to arrange whether or not to do shit"""
21 return True
22
23 # These calls need to be abstracted further, if I can do that then I can convince this module
24 # to update itself on the fly
25
26 Modules = {}
27
28 def mangle(name):
29 if len(name) < 8:
30 name += "_"
31 else:
32 try:
33 myno = int(name[7:])+1
34 name = "%s%.2i" % (name[:6], myno)
35 except ValueError:
36 name = name[:6] + "00"
37 return name
38
39 def _load_modules():
40 global Modules
41 global bModules
42 # This hook generates our bModules instance.
43 regen_modules.rebuild_bModules()
44
45 # Test for syntax errors...
46 try:
47 bModules = reload(bModules)
48 except SyntaxError as e:
49 tb = sys.exc_info()[2]
50 exc_type, exc_value, exc_tb = sys.exc_info()
51 raise ModulesDidntLoadDueToSyntax(exc_type, exc_value, exc_tb)
52 Modules = bModules.modules
53 _load_modules()
54
55 #RE_NICK_MATCH = re.compile(r":([A-Za-z0-9_-^`]+)!([A-Za-z0-9_-]+)@([A-Za-z0-9_\.-])")
56 RE_NICK_MATCH = re.compile(r":([^! ]+)!([^@ ]+)@([A-Za-z0-9_\.-]+)")
57 RE_INFO_MATCH = re.compile(r":([^! ]+)!([^@ ]+)@([A-Za-z0-9_\.-]+)")
58
59 class irc_data(object):
a87c1d1 @richo Docstrings
authored
60 """A proxy to coerce datatypes more appropriately for comparisons,
61 ultimately delegating to the data itself if it can't find anything better
62 to do with it"""
4ef1e83 @richo Fix borked newlines
authored
63 def __init__(self, data):
64 self.data = data
65
66 def __eq__(self, cmp):
67 if type(cmp) == int:
68 return self.data == cmp
69 elif type(cmp) == str:
70 return str(self.data) == cmp
71 else:
72 return self.data == cmp
73 IRC_MOTD_START = irc_data(375)
74 IRC_MOTD_DATA = irc_data(372)
75 IRC_NICK_IN_USE = irc_data(433)
76 IRC_NICK_NOT_AVAILABLE = irc_data(432)
77 IRC_TOPIC = irc_data(332)
78
79
80 class Message(object):
a87c1d1 @richo Docstrings
authored
81 """Reprsentation of a single message from an ircd"""
4ef1e83 @richo Fix borked newlines
authored
82 def __init__(self, msg):
83 self.msg = msg
84 self.data_segment = None
85 self.address_segment = None
86 self.nick = None
87 self.name = None
88 self.host = None
89 self.numeric = False
90 self.source, self.event, self.data = msg.split(" ", 2)
91 self.event = self.event.upper()
92 self.replyto = None
93 self.origin = None
94 try:
95 int(self.event)
96 self.numeric = True
97 if ":" in self.data:
98 self.address_segment, self.data_segment = [i.strip() for i in self.data.split(":", 1)]
99 except ValueError:
100 # For the most part, we can safely only look at stuff that's non-numeric
101 # Implies that self.event wasn't a number
102 if ":" in self.data:
103 self.address_segment, self.data_segment = [i.strip() for i in self.data.split(":", 1)]
104 else:
105 logging.fixme("No address segment: %s" % self.data)
106 self.data_segment = self.data
107 self.address_segment = "<NoAddress>"
108 # We go a bit further in attempting to gather info...
109 #:richo!richo@staffers.psych0tik.net
110 m = RE_NICK_MATCH.search(self.source)
111 if m:
112 self.nick = m.group(1)
113 self.name = m.group(2)
114 self.host = m.group(3)
115 # Hax to make this slightly more logical
116 if self.event == "JOIN":
117 self.address_segment = self.data_segment
118 # Hanlder hax
119 if self.event == "MODE":
120 self.address_segment = self.data_segment.split(" ", 1)[0]
121 if self.is_private():
122 self.replyto = self.nick
123 self.origin = 'privmsg'
124 else:
125 self.replyto = self.address_segment
126 self.origin = self.address_segment
127
128 def parse_modes(self):
129 if self.event != "MODE":
130 return None
131 channel, modes, nicks = self.data_segment.split(" ", 2)
132 return (channel, modes, nicks)
133 def is_private(self):
134 # This needs to be more global:
135 try:
136 if not self.address_segment[0] in ["!", "&", "#"]:
137 return True
138 except TypeError:
139 pass
cf9f6a5 @richo IndexErrors mean it's probably not private
authored
140 except IndexError:
141 pass
4ef1e83 @richo Fix borked newlines
authored
142 return False
143
144 def __str__(self):
145 return self.msg
146 def dump(self):
147 return " ".join(["Data Segment : %s" % (self.data_segment),
148 "Address Segment : %s" % (self.address_segment),
149 "Source : %s" % (self.source),
150 "Event : %s" % (self.event),
151 "Data : %s" % (self.data),
152 "Nick : %s" % (self.nick),
153 "Name : %s" % (self.name),
154 "Host : %s" % (self.host),
155 "--"])
156
157 class chatnet(object):
a87c1d1 @richo Docstrings
authored
158 """Representation of a single connection to an ircd"""
4ef1e83 @richo Fix borked newlines
authored
159 def __init__(self, host, port=6667, use_ssl=False):
160 self.auth_host = ''
161 self.auth_hash = ''
162 self.nick = ""
163
164 self.auth_host = config.auth_host
165 self.auth_hash = config.auth_hash
166 self.authenticator = auth.Authenticator(auth_hash = self.auth_hash, valid_host = self.auth_host)
167
168 self.ready_signal = IRC_MOTD_START
169 self.ready = False
170 self.data = ""
171 self._queue = []
172 self.queue = []
173 self.host = host
174 self.port = port
175 self.use_ssl = use_ssl
176 self.channels = {'privmsg': Channel("privmsg", self)}
177 self.sock = SockConnect(self.host, self.port, self.use_ssl)
178 self.msg = self.privmsg
179 self.chore_queue = []
180 self.event_handlers = {
181 'JOIN': self.handle_join
182 }
87bc297 @richo whitespace
authored
183
4ef1e83 @richo Fix borked newlines
authored
184 def recv_wait(self):
185 #This method pulls data back from the server and queues it for processing.
186 #So far it's considerations are:
187 #Don't do anything particularly clever with the last item, it may be incomplete
188 #Handle PING/PONG instantly.
189 #XXX TODO
190 buf = self.sock.recv(1024)
191 if not buf:
192 raise IrcDisconnected
193 self.data += buf
194 self._queue += self.data.split("\r\n")
195 if self.data.endswith("\r\n"):
196 self.data = ""
197 else:
198 self.data = self._queue.pop()
199 # continue with execution
87bc297 @richo whitespace
authored
200
4ef1e83 @richo Fix borked newlines
authored
201 def _handle(self, msg):
202 # XXX This desperately needs fleshing out
87bc297 @richo whitespace
authored
203 # if addressed to channel
4ef1e83 @richo Fix borked newlines
authored
204 # -> locate channel object and add to queue
205 # -- If not joined to channel, add channel object and populate queue
206 # -> if in doubt, dump to main queue
207 # -> Debug condition, self.debug everything-
208
209 # The _handle method also need
210
211 if msg.upper().startswith("PING"):
2dd541b @richo Respond to pings instantly
authored
212 self._write(msg.upper().replace("PING", "PONG"))
4ef1e83 @richo Fix borked newlines
authored
213 return
214 message = Message(msg)
215 # Have code for catching identify here.
216 # IF ident successful
217 # -> Set self.nick
218 # IF ident failed
219 # -> Mangle nick and try again. ## NEEDS NICK TO DO THIS. Set nick in identify
220 if message.event == IRC_TOPIC:
221 self.handle_topic(message)
222 if message.event == IRC_NICK_IN_USE:
223 self.retry_identify()
224 if message.event == IRC_NICK_NOT_AVAILABLE:
225 self.retry_identify()
226
227 if message.event == self.ready_signal:
228 self.ready = True
229
230 # Start handling.
231 # TODO, do whatever hax need to be done to populate the channel's topic element.
232 # Perfectly valid to have the channel itself put in a request for the topic in initialisation
233
234 if message.event in self.event_handlers:
235 self.event_handlers[message.event](message)
236
237 try:
238 chan = self.channels[message.address_segment]
239 chan.add_msg(message)
240 except:
241 # This creates channels for EVERYONE that privmsg's us.
242 # I'm not sure this is right, should privmsg's from nonchannels just go into an arbitrary queue?
243 # Ignore stuff for channels we don't know about.
244 self.channels['privmsg'].add_msg(message)
245 def handle_join(self, msg):
246 self.add_channel(msg.data_segment)
247
248 def handle_topic(self, msg):
249 #Source : :natalya.psych0tik.net
250 #Event : 332
251 #Data : pyBawt_ #rawptest :OBVIOUS TOPIC STRING
252 nick, chan = msg.address_segment.split(" ")[0:2]
253 topic = msg.data_segment
254 if chan in self.channels:
255 self.channels[chan].topic = topic
256
257 def add_channel(self, channel):
258 if channel not in self.channels:
259 self.channels[channel] = Channel(channel, self)
260
261 def dump_queue(self):
262 while self._queue:
263 msg = self._queue.pop(0)
264 if not msg:
265 continue
266 self._handle(msg)
267 for i in self.channels.values():
268 i.do_chores()
269 self._do_chores()
270
271 def dump_channel_data(self):
272 out = []
273 for i in self.channels:
274 out.append(i)
275 out.append(repr(self.channels[i].modules))
276 return out
277
278 def available_modules(self):
279 out = []
280 for i in dir(bModules):
281 if i.endswith("Module"):
282 out.append(i)
283 return out
284
285 def retry_identify(self):
286 # We have been denied our nick, work out what to do.
287 if self.nickserv_info:
288 # We have nickserv info, so try to boot our ghost, however
289 # We need a valid nick to do this. TODO
290 pass
291 else:
292 self.identify(mangle(self.nick), self.nickserv_info)
293 # This call is precariously close to becoming recursive.
294 # Make sure that identify never has a direct call path here.
87bc297 @richo whitespace
authored
295
4ef1e83 @richo Fix borked newlines
authored
296 def auth_self(self, to, pas):
297 # privmsg bypasses the chores code, it seems
298 self.add_chore(self.privmsg, (to, "IDENTIFY %s" % pas))
299
300 def identify(self, nick, nickserv_info=None):
301 # TODO Have this do something a bit more intelligent
302 # If setting nick fails (trigger based on the queue, try again)
303 # Use the chore system
304 self.nickserv_info = nickserv_info
305 self.nick = nick
306 # We use _write because if we wait for readyness we'll be waiting a while..
307 self._write("USER %(nick)s * 8 :%(nick)s" % {"nick": nick})
308 self._write("NICK %(nick)s" % {"nick": nick})
309 # If we have ended up here and we have nickserv_info, one of two things has happened.
310 # a) our nick is taken and we need to change nick, so we can talk to nickserv, then arrange a call to this function
311 # to get our nick back and ID
312 # b) We have our nick (either because it was free or because we have ghosted our old nick)
313 # Either way, that should be handled out of _handle for an appropriate signal, potentially off the MOTD signal
314 # That tells us that we're ready
315 if self.nickserv_info:
316 self.ns_identify
317
318 def notice(self, to, msg):
319 # Should this be in the queue?
320 self.write("NOTICE %(to)s :%(msg)s" % {"to": to, "msg": msg})
321
322 def privmsg(self, to, msg):
323 self._write("PRIVMSG %(to)s :%(msg)s" % {"to": to, "msg": msg})
324
325 def action(self, to, msg):
326 self._write("PRIVMSG %(to)s :\x01ACTION %(msg)s\x01" % {"to": to, "msg": msg})
327
328 def kick(self, chan, nick, reason=''):
329 self.write("KICK %(chan)s %(nick)s :%(reason)s" % (
330 { 'chan': chan,
331 'nick': nick,
332 'reason': reason}))
333
334 def write(self, msg):
335 self.add_chore(self._write, [msg])
336
337 def _write(self, msg):
338 self.sock.send(msg + "\n")
339
340 def join(self, chan, key=""):
341 self.add_chore(self._join, [chan, key])
342
343 def part(self, chan, reason=""):
344 self.add_chore(self._part, [chan, reason])
345
346
347 def _part(self, chan, reason):
348 self._write("PART %(chan)s %(reason)s" % {'chan': chan, 'reason': reason})
349 # XXX Let a handler do this when we get notification from the server
350 # it should tell us our state, not let us dictate
351 if chan in self.channels:
352 del self.channels[chan]
353
354 def quit(self, quitmsg=""):
355 # Flush our queues before we leave.
356 self.write("QUIT :%s" % (quitmsg))
357 self._do_chores()
358 SockClose(self.sock)
359
360 def reload_modules(self):
361 """Reloads modules, returns true on success or a traceback object if shit hits the fan"""
362 try:
363 _load_modules()
364 except ModulesDidntLoadDueToSyntax as tb:
365 # TODO - this can't be right. Works, but looks all fucked up
366 # I'm positive that the native exception handling caters for this
367 return tb # Exception instance, contains exc_info
368 else:
369 for i in self.channels.values():
370 i.reload_modules()
371 return True
372 def reg_handler(self, signum, handler):
373 # Create a signal handler which inserts a call to handler into the chore stack
374 def _(*args):
375 # This is the function which will be called
376 self.add_chore(handler, [])
377 raise FlushQueue
378 signal.signal(signum, _)
379
380 def add_chore(self, method, args):
381 self.chore_queue.append((method, args))
382
383 def add_module(self, chan, module):
384 return self.channels[chan].add_module(module)
385
386 def del_module(self, chan, module):
387 try:
388 self.channels[chan].del_module(module)
389 except ModuleError:
390 print "Couldn't remove %s from %s" % (module, chan)
391 raise
392
393 def _join(self, chan, key=""):
394 if chan not in self.channels:
395 self.write("JOIN %(chan)s %(key)s" % {"chan": chan,
396 "key" : key})
397
398 # self.channels[chan] = channel()
399 # The recv parser will handle adding it, once we're actually joined.
400
401 def _do_chores(self):
402 ret = False
403 if not self.ready:
404 return
405 while self.chore_queue:
406 ret = True
407 chore = self.chore_queue.pop(0)
408 chore[0](* chore[1])
409 return ret
410
411 class Nick(object):
412 # Dummy holder for nick properties
413 op = False
414 voice = False
415
416 class Channel(object):
417 """Placeholder until I actually have something of use"""
418 def __init__(self, name, parent):
419 self.name = name.lower()
420 self.parent = parent
421 self.queue = []
422 self.mode = []
423 self.cmode = []
424 self.standing = []
425 self.modules = []
426 self.init_modules()
427 self.topic = ""
428 #self.get_topic()
429 self.modes = {
430 'op': False,
431 'halfop': False,
432 'voice': False,
433 'owner': False,
434 'admin': False
435 }
436
437 def privmsg(self, msg):
438 """Hook for passing messages back when I have a channel, but not a parent"""
439 self.parent.privmsg(self.name, msg)
440 def mode_plus_b(self):
441 self.mode(self.nick, "+B")
442
443 def init_modules(self):
444 try:
445 for i in Modules[self.name]:
446 self.modules.append(i(self.parent, self))
447 except KeyError:
448 # No modules for this channel..
449 pass
450 def get_topic(self):
451 """Fire off a request for topic. This will be interpreted elsewhere"""
452 self.parent.write("TOPIC %(name)s" % {'name': self.name})
453
454 def set_topic(self, topic):
455 """Set the topic for this channel"""
456 self.parent.write("TOPIC %(name)s :%(topic)s" % {'name': self.name, 'topic': topic})
457 # TODO - Need to retrieve channel modes
458 # no +t means we don't need ops
459 return self.modes['op']
460
461 def reload_modules(self):
462 _load_modules()
463 self.modules = []
464 self.init_modules()
465
466 def add_module(self, module):
467 # Really haxy test for modules already loaded
468 try:
469 mod = getattr(bModules, module)
470 for i in self.modules:
471 logging.fixme("Scanning %s against %s" % (i, mod))
472 if isinstance(i, mod):
473 raise bModules.ModuleAlreadyLoaded
474 self.modules.append(mod(self.parent, self))
475 return True
476 except AttributeError:
477 return False
478
479 def dump_modules(self):
480 return ", ".join(repr(i) for i in self.modules)
481
482 def del_module(self, module):
483 m = getattr(bModules, module)
484 l = len(self.modules)
485 self.modules[:] = [i for i in self.modules if type(i) != type(m)]
486 if len(self.modules) == l:
487 raise ModuleError
488 return
87bc297 @richo whitespace
authored
489
4ef1e83 @richo Fix borked newlines
authored
490 def add_msg(self, msg):
491 self.queue.append(msg)
492
493 def handle_mode(self, msg):
494 m_map = {'+': True, '-': False}
495 o_map = {'o': 'op',
496 'v': 'voice',
497 'h': 'halfop',
498 'a': 'admin',
499 'q': 'owner'}
500 try:
501 chan, modes, nicks = msg.data_segment.split(" ", 2)
502 except:
503 # I don't know why non-channel mode events are winding up here
504 return
505 nicks = nicks.split(" ")
506 action = 'True'
507 for m in modes:
508 try:
509 action = m_map[m]
510 except KeyError:
511 nick = nicks.pop(0)
512 try:
513 if nick == self.parent.nick:
514 self.modes[o_map[m]] = action
515 else:
516 # TODO do something clever when other people's modes are changed.
517 pass
518 except KeyError:
519 # unrecognised mode.
520 print "Unrecognised mode!"
521 msg.dump()
522 def dump_modes(self):
523 return "%s: %s" % (self.name, repr(self.modes))
524
525 def do_chores(self):
526 # We load through a set of pluggable triggers.
527 # I have not done this yet, but I'm gunna....
528 while self.queue:
529 msg = self.queue.pop(0)
530 # Handle mode changes internally to update status
531 if msg.event == "MODE":
532 self.handle_mode(msg)
533 for i in self.modules:
534 if i.want(msg):
535 try:
536 # TODO - implement a signal stop
537 i.handle(msg)
5a7b0a1 @richo Merge branch 'universal_exceptions'
authored
538 except StopHandling:
4ef1e83 @richo Fix borked newlines
authored
539 break
540
87bc297 @richo whitespace
authored
541 def SockConnect(host, port, use_ssl):
4ef1e83 @richo Fix borked newlines
authored
542 addr = (host, port)
543 sock = None
544 for res in socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM):
545 af, socktype, proto, canonname, sa = res
546 try:
547 sock = socket.socket(af, socktype, proto)
548 if use_ssl:
549 sock = ssl.wrap_socket(sock)
550 except socket.error, msg:
551 sock = None
552 continue
553 try:
554 sock.connect(sa)
555 except socket.error, msg:
556 sock.close()
557 sock = None
558 continue
559 break
560 if sock is None:
561 raise RuntimeError, "could not connect socket"
562 #sock.settimeout(1)
563 return sock
564
565 def SockClose(sock):
566 try:
567 sock.shutdown(socket.SHUT_RDWR)
568 sock.close()
569 except:
570 pass
Something went wrong with that request. Please try again.