]> jfr.im git - irc/quakenet/qwebirc.git/blame - qwebirc/ajaxengine.py
Fix actions.
[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 43 if len(self.subscriptions) >= config.MAXSUBSCRIPTIONS:
4e221566 44 self.subscriptions.pop(0).close()
0df6faa6 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
4e221566
CP
114 def close(self):
115 self.request.finish()
116
9e769c12
CP
117class MultipleUseChannel(Channel):
118 def write(self, data):
119 self.request.write(data)
120 return True
121
122class AJAXEngine(resource.Resource):
123 isLeaf = True
124
125 def __init__(self, prefix):
126 self.prefix = prefix
127
128 @jsondump
57ea572e 129 def render_POST(self, request):
9e769c12
CP
130 path = request.path[len(self.prefix):]
131 if path == "/n":
132 ip = request.transport.getPeer()
133 ip = ip[1]
134
135 nick, ident = request.args.get("nick"), "webchat"
136 if not nick:
137 return [False, "Nickname not supplied"]
138
139 nick = nick[0]
4094890f
CP
140
141 for i in xrange(10):
142 id = get_session_id()
143 if not Sessions.get(id):
144 break
145 else:
146 raise IDGenerationException()
147
9e769c12
CP
148 session = IRCSession(id)
149
4094890f 150 client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=config.REALNAME)
9e769c12
CP
151 session.client = client
152
153 Sessions[id] = session
154
57ea572e
CP
155 return [True, id]
156 return [False, "404"]
157
158 @jsondump
159 def render_GET(self, request):
160 path = request.path[len(self.prefix):]
9e769c12
CP
161 if path.startswith("/s/"):
162 sessionid = path[3:]
163 session = Sessions.get(sessionid)
164
165 if not session:
166 return [False, "Bad session ID"]
167
168 session.subscribe(SingleUseChannel(request))
169 return server.NOT_DONE_YET
170 if path.startswith("/p/"):
171 command = request.args.get("c")
172 if not command:
173 return [False, "No command specified"]
174
175 command = command[0]
176
177 sessionid = path[3:]
178 session = Sessions.get(sessionid)
179 if not session:
180 return [False, "Bad session ID"]
181
182 try:
183 decoded = command.decode("utf-8")
184 except UnicodeDecodeError:
185 decoded = command.decode("iso-8859-1", "ignore")
8dc46dfa 186
341581c4
CP
187 if len(decoded) > config.MAXLINELEN:
188 session.disconnect()
189 return [False, "Line too long"]
190
8dc46dfa
CP
191 try:
192 session.push(decoded)
193 except AttributeError: # occurs when we haven't noticed an error
194 session.disconnect()
195 return [False, "Connection closed by server."]
196 except Exception, e: # catch all
197 session.disconnect()
198 traceback.print_exc(file=sys.stderr)
199 return [False, "Unknown error."]
200
9e769c12
CP
201 return [True]
202
203 return [False, "404"]