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