329 lines
11 KiB
Python
329 lines
11 KiB
Python
|
import socket
|
||
|
import sys
|
||
|
|
||
|
from conf import conf
|
||
|
import ui
|
||
|
import windows
|
||
|
import info
|
||
|
|
||
|
DISCONNECTED = 0
|
||
|
CONNECTING = 1
|
||
|
INITIALIZING = 2
|
||
|
CONNECTED = 3
|
||
|
|
||
|
def parse_irc(msg, server):
|
||
|
msg = msg.split(' ')
|
||
|
|
||
|
# if our very first character is :
|
||
|
# then this is the source,
|
||
|
# otherwise insert the server as the source
|
||
|
if msg and msg[0].startswith(':'):
|
||
|
msg[0] = msg[0][1:]
|
||
|
else:
|
||
|
msg.insert(0, server)
|
||
|
|
||
|
# loop through the msg until we find
|
||
|
# something beginning with :
|
||
|
for i, token in enumerate(msg):
|
||
|
if token.startswith(':'):
|
||
|
# remove the :
|
||
|
msg[i] = msg[i][1:]
|
||
|
|
||
|
# join up the rest
|
||
|
msg[i:] = [' '.join(msg[i:])]
|
||
|
break
|
||
|
|
||
|
# filter out the empty pre-":" tokens and add on the text to the end
|
||
|
return [m for m in msg[:-1] if m] + msg[-1:]
|
||
|
|
||
|
# note: this sucks and makes very little sense, but it matches the BNF
|
||
|
# as far as we've tested, which seems to be the goal
|
||
|
|
||
|
def default_nicks():
|
||
|
try:
|
||
|
nicks = [conf.get('nick')] + conf.get('altnicks',[])
|
||
|
if not nicks[0]:
|
||
|
import getpass
|
||
|
nicks = [getpass.getuser()]
|
||
|
except:
|
||
|
nicks = ["mrurk"]
|
||
|
return nicks
|
||
|
|
||
|
class Network(object):
|
||
|
socket = None
|
||
|
|
||
|
def __init__(self, core, server="irc.default.org", port=6667, nicks=[],
|
||
|
username="", fullname="", name=None, **kwargs):
|
||
|
self.manager = core.manager
|
||
|
self.server = server
|
||
|
self.port = port
|
||
|
self.events = core.events
|
||
|
|
||
|
self.name = name or server
|
||
|
|
||
|
self.nicks = nicks or default_nicks()
|
||
|
self.me = self.nicks[0]
|
||
|
|
||
|
self.username = username or "urk"
|
||
|
self.fullname = fullname or conf.get("fullname", self.username)
|
||
|
self.password = ''
|
||
|
|
||
|
self.isupport = {
|
||
|
'NETWORK': server,
|
||
|
'PREFIX': '(ohv)@%+',
|
||
|
'CHANMODES': 'b,k,l,imnpstr',
|
||
|
}
|
||
|
self.prefixes = {'o':'@', 'h':'%', 'v':'+', '@':'o', '%':'h', '+':'v'}
|
||
|
|
||
|
self.status = DISCONNECTED
|
||
|
self.failedhosts = [] #hosts we've tried and failed to connect to
|
||
|
self.channel_prefixes = '&#+$' # from rfc2812
|
||
|
|
||
|
self.on_channels = set()
|
||
|
self.requested_joins = set()
|
||
|
self.requested_parts = set()
|
||
|
|
||
|
self.buffer = ''
|
||
|
|
||
|
#called when we get a result from the dns lookup
|
||
|
def on_dns(self, result, error):
|
||
|
if error:
|
||
|
self.disconnect(error=error[1])
|
||
|
else:
|
||
|
#import os
|
||
|
#import random
|
||
|
#random.seed()
|
||
|
#random.shuffle(result)
|
||
|
if socket.has_ipv6: #prefer ipv6
|
||
|
result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f == socket.AF_INET6]+result
|
||
|
elif hasattr(socket,"AF_INET6"): #ignore ipv6
|
||
|
result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f != socket.AF_INET6]
|
||
|
|
||
|
self.failedlasthost = False
|
||
|
|
||
|
for f, t, p, c, a in result:
|
||
|
if (f, t, p, c, a) not in self.failedhosts:
|
||
|
try:
|
||
|
self.socket = socket.socket(f, t, p)
|
||
|
except:
|
||
|
continue
|
||
|
self.source = ui.fork(self.on_connect, self.socket.connect, a)
|
||
|
self.failedhosts.append((f, t, p, c, a))
|
||
|
if set(self.failedhosts) >= set(result):
|
||
|
self.failedlasthost = True
|
||
|
break
|
||
|
else:
|
||
|
self.failedlasthost = True
|
||
|
if len(result):
|
||
|
self.failedhosts[:] = (f, t, p, c, a),
|
||
|
f, t, p, c, a = result[0]
|
||
|
try:
|
||
|
self.socket = socket.socket(f, t, p)
|
||
|
self.source = ui.fork(self.on_connect, self.socket.connect, a)
|
||
|
except:
|
||
|
self.disconnect(error="Couldn't find a host we can connect to")
|
||
|
else:
|
||
|
self.disconnect(error="Couldn't find a host we can connect to")
|
||
|
|
||
|
#called when socket.open() returns
|
||
|
def on_connect(self, result, error):
|
||
|
if error:
|
||
|
self.disconnect(error=error[1])
|
||
|
#we should immediately retry if we failed to open the socket and there are hosts left
|
||
|
if self.status == DISCONNECTED and not self.failedlasthost:
|
||
|
windows.get_default(self).write("* Retrying with next available host")
|
||
|
self.connect()
|
||
|
else:
|
||
|
self.source = source = ui.Source()
|
||
|
self.status = INITIALIZING
|
||
|
self.failedhosts[:] = ()
|
||
|
|
||
|
self.events.trigger('SocketConnect', network=self)
|
||
|
|
||
|
if source.enabled:
|
||
|
self.source = ui.fork(self.on_read, self.socket.recv, 8192)
|
||
|
|
||
|
#called when we read data or failed to read data
|
||
|
def on_read(self, result, error):
|
||
|
if error:
|
||
|
self.disconnect(error=error[1])
|
||
|
elif not result:
|
||
|
self.disconnect(error="Connection closed by remote host")
|
||
|
else:
|
||
|
self.source = source = ui.Source()
|
||
|
|
||
|
self.buffer = (self.buffer + result).split("\r\n")
|
||
|
|
||
|
for line in self.buffer[:-1]:
|
||
|
self.got_msg(line)
|
||
|
|
||
|
if self.buffer:
|
||
|
self.buffer = self.buffer[-1]
|
||
|
else:
|
||
|
self.buffer = ''
|
||
|
|
||
|
if source.enabled:
|
||
|
self.source = ui.fork(self.on_read, self.socket.recv, 8192)
|
||
|
|
||
|
def raw(self, msg):
|
||
|
self.events.trigger("OwnRaw", network=self, raw=msg)
|
||
|
|
||
|
if self.status >= INITIALIZING:
|
||
|
self.socket.send(msg + "\r\n")
|
||
|
|
||
|
def got_msg(self, msg):
|
||
|
pmsg = parse_irc(msg, self.server)
|
||
|
|
||
|
e_data = self.events.data(
|
||
|
raw=msg,
|
||
|
msg=pmsg,
|
||
|
text=pmsg[-1],
|
||
|
network=self,
|
||
|
window=windows.get_default(self, self.manager)
|
||
|
)
|
||
|
|
||
|
if "!" in pmsg[0]:
|
||
|
e_data.source, e_data.address = pmsg[0].split('!',1)
|
||
|
|
||
|
else:
|
||
|
e_data.source, e_data.address = pmsg[0], ''
|
||
|
|
||
|
if len(pmsg) > 2:
|
||
|
e_data.target = pmsg[2]
|
||
|
else:
|
||
|
e_data.target = pmsg[-1]
|
||
|
|
||
|
self.events.trigger('Raw', e_data)
|
||
|
|
||
|
def connect(self):
|
||
|
if not self.status:
|
||
|
self.status = CONNECTING
|
||
|
|
||
|
self.source = ui.fork(self.on_dns, socket.getaddrinfo, self.server, self.port, 0, socket.SOCK_STREAM)
|
||
|
|
||
|
self.events.trigger('Connecting', network=self)
|
||
|
|
||
|
def disconnect(self, error=None):
|
||
|
if self.socket:
|
||
|
self.socket.close()
|
||
|
|
||
|
if self.source:
|
||
|
self.source.unregister()
|
||
|
self.source = None
|
||
|
|
||
|
self.socket = None
|
||
|
|
||
|
self.status = DISCONNECTED
|
||
|
|
||
|
#note: connecting from onDisconnect is probably a Bad Thing
|
||
|
self.events.trigger('Disconnect', network=self, error=error)
|
||
|
|
||
|
#trigger a nick change if the nick we want is different from the one we
|
||
|
# had.
|
||
|
if self.me != self.nicks[0]:
|
||
|
self.events.trigger(
|
||
|
'Nick', network=self, window=windows.get_default(self),
|
||
|
source=self.me, target=self.nicks[0], address='',
|
||
|
text=self.nicks[0]
|
||
|
)
|
||
|
self.me = self.nicks[0]
|
||
|
|
||
|
def norm_case(self, string):
|
||
|
return string.lower()
|
||
|
|
||
|
def quit(self, msg=None):
|
||
|
if self.status:
|
||
|
try:
|
||
|
if msg == None:
|
||
|
msg = conf.get('quitmsg', "%s - %s" % (info.long_version, info.website))
|
||
|
self.raw("QUIT :%s" % msg)
|
||
|
except:
|
||
|
pass
|
||
|
self.disconnect()
|
||
|
|
||
|
def join(self, target, key='', requested=True):
|
||
|
if key:
|
||
|
key = ' '+key
|
||
|
self.raw("JOIN %s%s" % (target,key))
|
||
|
if requested:
|
||
|
for chan in target.split(' ',1)[0].split(','):
|
||
|
if chan == '0':
|
||
|
self.requested_parts.update(self.on_channels)
|
||
|
else:
|
||
|
self.requested_joins.add(self.norm_case(chan))
|
||
|
|
||
|
def part(self, target, msg="", requested=True):
|
||
|
if msg:
|
||
|
msg = " :" + msg
|
||
|
|
||
|
self.raw("PART %s%s" % (target, msg))
|
||
|
if requested:
|
||
|
for chan in target.split(' ',1)[0].split(','):
|
||
|
self.requested_parts.add(self.norm_case(target))
|
||
|
|
||
|
def msg(self, target, msg):
|
||
|
self.raw("PRIVMSG %s :%s" % (target, msg))
|
||
|
|
||
|
self.events.trigger(
|
||
|
'OwnText', source=self.me, target=str(target), text=msg,
|
||
|
network=self, window=windows.get_default(self, self.manager)
|
||
|
)
|
||
|
|
||
|
def notice(self, target, msg):
|
||
|
self.raw("NOTICE %s :%s" % (target, msg))
|
||
|
|
||
|
self.events.trigger(
|
||
|
'OwnNotice', source=self.me, target=str(target), text=msg,
|
||
|
network=self, window=windows.get_default(self)
|
||
|
)
|
||
|
|
||
|
#a Network that is never connected
|
||
|
class DummyNetwork(Network):
|
||
|
def __nonzero__(self):
|
||
|
return False
|
||
|
|
||
|
def __init__(self, core):
|
||
|
Network.__init__(self, core)
|
||
|
|
||
|
self.name = self.server = self.isupport['NETWORK'] = "None"
|
||
|
|
||
|
def connect(self):
|
||
|
raise NotImplementedError, "Cannot connect dummy network."
|
||
|
|
||
|
def raw(self, msg):
|
||
|
raise NotImplementedError, "Cannot send %s over the dummy network." % repr(msg)
|
||
|
|
||
|
#dummy_network = DummyNetwork()
|
||
|
|
||
|
#this was ported from srvx's tools.c
|
||
|
def match_glob(text, glob, t=0, g=0):
|
||
|
while g < len(glob):
|
||
|
if glob[g] in '?*':
|
||
|
star_p = q_cnt = 0
|
||
|
while g < len(glob):
|
||
|
if glob[g] == '*':
|
||
|
star_p = True
|
||
|
elif glob[g] == '?':
|
||
|
q_cnt += 1
|
||
|
else:
|
||
|
break
|
||
|
g += 1
|
||
|
t += q_cnt
|
||
|
if t > len(text):
|
||
|
return False
|
||
|
if star_p:
|
||
|
if g == len(glob):
|
||
|
return True
|
||
|
for i in xrange(t, len(text)):
|
||
|
if text[i] == glob[g] and match_glob(text, glob, i+1, g+1):
|
||
|
return True
|
||
|
return False
|
||
|
else:
|
||
|
if t == len(text) and g == len(glob):
|
||
|
return True
|
||
|
if t == len(text) or g == len(glob) or text[t] != glob[g]:
|
||
|
return False
|
||
|
t += 1
|
||
|
g += 1
|
||
|
return t == len(text)
|