conf: use shutil.move instead of os.rename for saving
[urk.git] / irc.py
blob6deb7d7b4eb5ab8390ee0de1c6852c602530ba89
1 import socket
2 import sys
4 from conf import conf
5 import events
6 import urk
7 import ui
8 import windows
10 DISCONNECTED = 0
11 CONNECTING = 1
12 INITIALIZING = 2
13 CONNECTED = 3
15 def parse_irc(msg, server):
16 msg = msg.split(' ')
18 # if our very first character is :
19 # then this is the source,
20 # otherwise insert the server as the source
21 if msg and msg[0].startswith(':'):
22 msg[0] = msg[0][1:]
23 else:
24 msg.insert(0, server)
26 # loop through the msg until we find
27 # something beginning with :
28 for i, token in enumerate(msg):
29 if token.startswith(':'):
30 # remove the :
31 msg[i] = msg[i][1:]
33 # join up the rest
34 msg[i:] = [' '.join(msg[i:])]
35 break
37 # filter out the empty pre-":" tokens and add on the text to the end
38 return [m for m in msg[:-1] if m] + msg[-1:]
40 # note: this sucks and makes very little sense, but it matches the BNF
41 # as far as we've tested, which seems to be the goal
43 def default_nicks():
44 try:
45 nicks = [conf.get('nick')] + conf.get('altnicks',[])
46 if not nicks[0]:
47 import getpass
48 nicks = [getpass.getuser()]
49 except:
50 nicks = ["mrurk"]
51 return nicks
53 class Network(object):
54 socket = None
56 def __init__(self, server="irc.default.org", port=6667, nicks=[],
57 username="", fullname="", name=None, **kwargs):
58 self.server = server
59 self.port = port
61 self.name = name or server
63 self.nicks = nicks or default_nicks()
64 self.me = self.nicks[0]
66 self.username = username or "urk"
67 self.fullname = fullname or conf.get("fullname", self.username)
68 self.password = ''
70 self.isupport = {
71 'NETWORK': server,
72 'PREFIX': '(ohv)@%+',
73 'CHANMODES': 'b,k,l,imnpstr',
75 self.prefixes = {'o':'@', 'h':'%', 'v':'+', '@':'o', '%':'h', '+':'v'}
77 self.status = DISCONNECTED
78 self.failedhosts = [] #hosts we've tried and failed to connect to
79 self.channel_prefixes = '&#+$' # from rfc2812
81 self.on_channels = set()
82 self.requested_joins = set()
83 self.requested_parts = set()
85 self.buffer = ''
87 #called when we get a result from the dns lookup
88 def on_dns(self, result, error):
89 if error:
90 self.disconnect(error=error[1])
91 else:
92 #import os
93 #import random
94 #random.seed()
95 #random.shuffle(result)
96 if socket.has_ipv6: #prefer ipv6
97 result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f == socket.AF_INET6]+result
98 elif hasattr(socket,"AF_INET6"): #ignore ipv6
99 result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f != socket.AF_INET6]
101 self.failedlasthost = False
103 for f, t, p, c, a in result:
104 if (f, t, p, c, a) not in self.failedhosts:
105 try:
106 self.socket = socket.socket(f, t, p)
107 except:
108 continue
109 self.source = ui.fork(self.on_connect, self.socket.connect, a)
110 self.failedhosts.append((f, t, p, c, a))
111 if set(self.failedhosts) >= set(result):
112 self.failedlasthost = True
113 break
114 else:
115 self.failedlasthost = True
116 if len(result):
117 self.failedhosts[:] = (f, t, p, c, a),
118 f, t, p, c, a = result[0]
119 try:
120 self.socket = socket.socket(f, t, p)
121 self.source = ui.fork(self.on_connect, self.socket.connect, a)
122 except:
123 self.disconnect(error="Couldn't find a host we can connect to")
124 else:
125 self.disconnect(error="Couldn't find a host we can connect to")
127 #called when socket.open() returns
128 def on_connect(self, result, error):
129 if error:
130 self.disconnect(error=error[1])
131 #we should immediately retry if we failed to open the socket and there are hosts left
132 if self.status == DISCONNECTED and not self.failedlasthost:
133 windows.get_default(self).write("* Retrying with next available host")
134 self.connect()
135 else:
136 self.source = source = ui.Source()
137 self.status = INITIALIZING
138 self.failedhosts[:] = ()
140 events.trigger('SocketConnect', network=self)
142 if source.enabled:
143 self.source = ui.fork(self.on_read, self.socket.recv, 8192)
145 #called when we read data or failed to read data
146 def on_read(self, result, error):
147 if error:
148 self.disconnect(error=error[1])
149 elif not result:
150 self.disconnect(error="Connection closed by remote host")
151 else:
152 self.source = source = ui.Source()
154 self.buffer = (self.buffer + result).split("\r\n")
156 for line in self.buffer[:-1]:
157 self.got_msg(line)
159 if self.buffer:
160 self.buffer = self.buffer[-1]
161 else:
162 self.buffer = ''
164 if source.enabled:
165 self.source = ui.fork(self.on_read, self.socket.recv, 8192)
167 def raw(self, msg):
168 events.trigger("OwnRaw", network=self, raw=msg)
170 if self.status >= INITIALIZING:
171 self.socket.send(msg + "\r\n")
173 def got_msg(self, msg):
174 pmsg = parse_irc(msg, self.server)
176 e_data = events.data(
177 raw=msg,
178 msg=pmsg,
179 text=pmsg[-1],
180 network=self,
181 window=windows.get_default(self)
184 if "!" in pmsg[0]:
185 e_data.source, e_data.address = pmsg[0].split('!',1)
187 else:
188 e_data.source, e_data.address = pmsg[0], ''
190 if len(pmsg) > 2:
191 e_data.target = pmsg[2]
192 else:
193 e_data.target = pmsg[-1]
195 events.trigger('Raw', e_data)
197 def connect(self):
198 if not self.status:
199 self.status = CONNECTING
201 self.source = ui.fork(self.on_dns, socket.getaddrinfo, self.server, self.port, 0, socket.SOCK_STREAM)
203 events.trigger('Connecting', network=self)
205 def disconnect(self, error=None):
206 if self.socket:
207 self.socket.close()
209 if self.source:
210 self.source.unregister()
211 self.source = None
213 self.socket = None
215 self.status = DISCONNECTED
217 #note: connecting from onDisconnect is probably a Bad Thing
218 events.trigger('Disconnect', network=self, error=error)
220 #trigger a nick change if the nick we want is different from the one we
221 # had.
222 if self.me != self.nicks[0]:
223 events.trigger(
224 'Nick', network=self, window=windows.get_default(self),
225 source=self.me, target=self.nicks[0], address='',
226 text=self.nicks[0]
228 self.me = self.nicks[0]
230 def norm_case(self, string):
231 return string.lower()
233 def quit(self, msg=None):
234 if self.status:
235 try:
236 if msg == None:
237 msg = conf.get('quitmsg', "%s - %s" % (urk.long_version, urk.website))
238 self.raw("QUIT :%s" % msg)
239 except:
240 pass
241 self.disconnect()
243 def join(self, target, key='', requested=True):
244 if key:
245 key = ' '+key
246 self.raw("JOIN %s%s" % (target,key))
247 if requested:
248 for chan in target.split(' ',1)[0].split(','):
249 if chan == '0':
250 self.requested_parts.update(self.on_channels)
251 else:
252 self.requested_joins.add(self.norm_case(chan))
254 def part(self, target, msg="", requested=True):
255 if msg:
256 msg = " :" + msg
258 self.raw("PART %s%s" % (target, msg))
259 if requested:
260 for chan in target.split(' ',1)[0].split(','):
261 self.requested_parts.add(self.norm_case(target))
263 def msg(self, target, msg):
264 self.raw("PRIVMSG %s :%s" % (target, msg))
266 events.trigger(
267 'OwnText', source=self.me, target=str(target), text=msg,
268 network=self, window=windows.get_default(self)
271 def notice(self, target, msg):
272 self.raw("NOTICE %s :%s" % (target, msg))
274 events.trigger(
275 'OwnNotice', source=self.me, target=str(target), text=msg,
276 network=self, window=windows.get_default(self)
279 #a Network that is never connected
280 class DummyNetwork(Network):
281 def __nonzero__(self):
282 return False
284 def __init__(self):
285 Network.__init__(self)
287 self.name = self.server = self.isupport['NETWORK'] = "None"
289 def connect(self):
290 raise NotImplementedError, "Cannot connect dummy network."
292 def raw(self, msg):
293 raise NotImplementedError, "Cannot send %s over the dummy network." % repr(msg)
295 dummy_network = DummyNetwork()
297 #this was ported from srvx's tools.c
298 def match_glob(text, glob, t=0, g=0):
299 while g < len(glob):
300 if glob[g] in '?*':
301 star_p = q_cnt = 0
302 while g < len(glob):
303 if glob[g] == '*':
304 star_p = True
305 elif glob[g] == '?':
306 q_cnt += 1
307 else:
308 break
309 g += 1
310 t += q_cnt
311 if t > len(text):
312 return False
313 if star_p:
314 if g == len(glob):
315 return True
316 for i in xrange(t, len(text)):
317 if text[i] == glob[g] and match_glob(text, glob, i+1, g+1):
318 return True
319 return False
320 else:
321 if t == len(text) and g == len(glob):
322 return True
323 if t == len(text) or g == len(glob) or text[t] != glob[g]:
324 return False
325 t += 1
326 g += 1
327 return t == len(text)