]> jfr.im git - irc/quakenet/qwebirc.git/blame - qwebirc/ajaxengine.py
Prettify the embedded wizard and fix it for IE.
[irc/quakenet/qwebirc.git] / qwebirc / ajaxengine.py
CommitLineData
9e769c12
CP
1from twisted.web import resource, server, static
2from twisted.names import client
3from twisted.internet import reactor
8dc46dfa
CP
4import traceback
5import simplejson, md5, sys, os, ircclient, time, config, weakref
9e769c12
CP
6
7Sessions = {}
8
9def get_session_id():
4e4bbf26 10 return md5.md5(os.urandom(16)).hexdigest()
8dc46dfa
CP
11
12class BufferOverflowException(Exception):
13 pass
14
f59585a7
CP
15class AJAXException(Exception):
16 pass
17
4094890f
CP
18class IDGenerationException(Exception):
19 pass
20
bdd008f9
CP
21NOT_DONE_YET = None
22
9e769c12
CP
23def jsondump(fn):
24 def decorator(*args, **kwargs):
f59585a7
CP
25 try:
26 x = fn(*args, **kwargs)
bdd008f9
CP
27 if x is None:
28 return server.NOT_DONE_YET
29 x = (True, x)
f59585a7 30 except AJAXException, e:
bdd008f9 31 x = (False, e[0])
f59585a7
CP
32
33 return simplejson.dumps(x)
9e769c12
CP
34 return decorator
35
8dc46dfa
CP
36def cleanupSession(id):
37 try:
38 del Sessions[id]
39 except KeyError:
40 pass
41
9e769c12
CP
42class IRCSession:
43 def __init__(self, id):
44 self.id = id
45 self.subscriptions = []
46 self.buffer = []
47 self.throttle = 0
48 self.schedule = None
8dc46dfa
CP
49 self.closed = False
50 self.cleanupschedule = None
51
9e769c12 52 def subscribe(self, channel):
0df6faa6 53 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
4e221566 54 self.subscriptions.pop(0).close()
0df6faa6 55
9e769c12
CP
56 self.subscriptions.append(channel)
57 self.flush()
58
59 def flush(self, scheduled=False):
60 if scheduled:
61 self.schedule = None
62
63 if not self.buffer or not self.subscriptions:
64 return
65
66 t = time.time()
67
68 if t < self.throttle:
69 if not self.schedule:
70 self.schedule = reactor.callLater(self.throttle - t, self.flush, True)
71 return
72 else:
73 # process the rest of the packet
74 if not scheduled:
75 if not self.schedule:
76 self.schedule = reactor.callLater(0, self.flush, True)
77 return
78
79 self.throttle = t + config.UPDATE_FREQ
80
81 encdata = simplejson.dumps(self.buffer)
82 self.buffer = []
83
84 newsubs = []
85 for x in self.subscriptions:
86 if x.write(encdata):
87 newsubs.append(x)
88
89 self.subscriptions = newsubs
8dc46dfa
CP
90 if self.closed and not self.subscriptions:
91 cleanupSession(self.id)
92
9e769c12 93 def event(self, data):
8dc46dfa
CP
94 bufferlen = sum(map(len, self.buffer))
95 if bufferlen + len(data) > config.MAXBUFLEN:
96 self.buffer = []
97 self.client.error("Buffer overflow")
98 return
99
9e769c12
CP
100 self.buffer.append(data)
101 self.flush()
102
103 def push(self, data):
8dc46dfa
CP
104 if not self.closed:
105 self.client.write(data)
106
107 def disconnect(self):
108 # keep the session hanging around for a few seconds so the
109 # client has a chance to see what the issue was
110 self.closed = True
111
112 reactor.callLater(5, cleanupSession, self.id)
113
9e769c12
CP
114class Channel:
115 def __init__(self, request):
116 self.request = request
117
118class SingleUseChannel(Channel):
119 def write(self, data):
120 self.request.write(data)
121 self.request.finish()
122 return False
123
4e221566
CP
124 def close(self):
125 self.request.finish()
126
9e769c12
CP
127class MultipleUseChannel(Channel):
128 def write(self, data):
129 self.request.write(data)
130 return True
131
132class AJAXEngine(resource.Resource):
133 isLeaf = True
134
135 def __init__(self, prefix):
136 self.prefix = prefix
137
138 @jsondump
57ea572e 139 def render_POST(self, request):
9e769c12 140 path = request.path[len(self.prefix):]
f59585a7
CP
141 if path[0] == "/":
142 handler = self.COMMANDS.get(path[1:])
143 if handler is not None:
144 return handler(self, request)
145 raise AJAXException("404")
146
147# def render_GET(self, request):
148# return self.render_POST(request)
149
150 def newConnection(self, request):
151 _, ip, port = request.transport.getPeer()
9e769c12 152
f59585a7
CP
153 nick, ident = request.args.get("nick"), "webchat"
154 if not nick:
155 raise AJAXException("Nickname not supplied")
9e769c12 156
f59585a7 157 nick = nick[0]
57ea572e 158
f59585a7
CP
159 for i in xrange(10):
160 id = get_session_id()
161 if not Sessions.get(id):
162 break
163 else:
164 raise IDGenerationException()
9e769c12 165
f59585a7 166 session = IRCSession(id)
9e769c12 167
f59585a7
CP
168 client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=config.REALNAME)
169 session.client = client
170
171 Sessions[id] = session
172
173 return id
174
175 def getSession(self, request):
176 sessionid = request.args.get("s")
177 if sessionid is None:
178 raise AJAXException("Bad session ID")
9e769c12 179
f59585a7
CP
180 session = Sessions.get(sessionid[0])
181 if not session:
182 raise AJAXException("Bad session ID")
183 return session
8dc46dfa 184
f59585a7
CP
185 def subscribe(self, request):
186 self.getSession(request).subscribe(SingleUseChannel(request))
bdd008f9 187 return NOT_DONE_YET
9e769c12 188
f59585a7
CP
189 def push(self, request):
190 command = request.args.get("c")
191 if command is None:
192 raise AJAXException("No command specified")
193
194 command = command[0]
195
196 session = self.getSession(request)
197
198 try:
199 decoded = command.decode("utf-8")
200 except UnicodeDecodeError:
201 decoded = command.decode("iso-8859-1", "ignore")
202
203 if len(decoded) > config.MAXLINELEN:
204 session.disconnect()
205 raise AJAXException("Line too long")
206
207 try:
208 session.push(decoded)
209 except AttributeError: # occurs when we haven't noticed an error
210 session.disconnect()
211 raise AJAXException("Connection closed by server.")
212 except Exception, e: # catch all
213 session.disconnect()
214 traceback.print_exc(file=sys.stderr)
215 raise AJAXException("Unknown error.")
216
217 return True
218
219 COMMANDS = dict(p=push, n=newConnection, s=subscribe)
220