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