]> jfr.im git - irc/quakenet/qwebirc.git/blob - qwebirc/ajaxengine.py
0da0f384514719c3b26d15e4d157b042c41a968e
[irc/quakenet/qwebirc.git] / qwebirc / ajaxengine.py
1 from twisted.web import resource, server, static
2 from twisted.names import client
3 from twisted.internet import reactor
4 import traceback
5 import simplejson, md5, sys, os, ircclient, time, config, weakref
6
7 Sessions = {}
8
9 def get_session_id():
10 return md5.md5(os.urandom(16)).hexdigest()
11
12 class BufferOverflowException(Exception):
13 pass
14
15 class AJAXException(Exception):
16 pass
17
18 class IDGenerationException(Exception):
19 pass
20
21 NOT_DONE_YET = None
22
23 def jsondump(fn):
24 def decorator(*args, **kwargs):
25 try:
26 x = fn(*args, **kwargs)
27 if x is None:
28 return server.NOT_DONE_YET
29 x = (True, x)
30 except AJAXException, e:
31 x = (False, e[0])
32
33 return simplejson.dumps(x)
34 return decorator
35
36 def cleanupSession(id):
37 try:
38 del Sessions[id]
39 except KeyError:
40 pass
41
42 class IRCSession:
43 def __init__(self, id):
44 self.id = id
45 self.subscriptions = []
46 self.buffer = []
47 self.throttle = 0
48 self.schedule = None
49 self.closed = False
50 self.cleanupschedule = None
51
52 def subscribe(self, channel):
53 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
54 self.subscriptions.pop(0).close()
55
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
90 if self.closed and not self.subscriptions:
91 cleanupSession(self.id)
92
93 def event(self, data):
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
100 self.buffer.append(data)
101 self.flush()
102
103 def push(self, data):
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
114 class Channel:
115 def __init__(self, request):
116 self.request = request
117
118 class SingleUseChannel(Channel):
119 def write(self, data):
120 self.request.write(data)
121 self.request.finish()
122 return False
123
124 def close(self):
125 self.request.finish()
126
127 class MultipleUseChannel(Channel):
128 def write(self, data):
129 self.request.write(data)
130 return True
131
132 class AJAXEngine(resource.Resource):
133 isLeaf = True
134
135 def __init__(self, prefix):
136 self.prefix = prefix
137
138 @jsondump
139 def render_POST(self, request):
140 path = request.path[len(self.prefix):]
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()
152
153 nick, ident = request.args.get("nick"), "webchat"
154 if not nick:
155 raise AJAXException("Nickname not supplied")
156
157 nick = nick[0]
158
159 for i in xrange(10):
160 id = get_session_id()
161 if not Sessions.get(id):
162 break
163 else:
164 raise IDGenerationException()
165
166 session = IRCSession(id)
167
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")
179
180 session = Sessions.get(sessionid[0])
181 if not session:
182 raise AJAXException("Bad session ID")
183 return session
184
185 def subscribe(self, request):
186 self.getSession(request).subscribe(SingleUseChannel(request))
187 return NOT_DONE_YET
188
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