1#!/usr/bin/env python
2#
3# Copyright 2009 Facebook
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import logging
18import tornado.escape
19import tornado.ioloop
20import tornado.web
21import os.path
22import uuid
23
24from tornado.concurrent import Future
25from tornado import gen
26from tornado.options import define, options, parse_command_line
27
28define("port", default=8888, help="run on the given port", type=int)
29define("debug", default=False, help="run in debug mode")
30
31
32class MessageBuffer(object):
33    def __init__(self):
34        self.waiters = set()
35        self.cache = []
36        self.cache_size = 200
37
38    def wait_for_messages(self, cursor=None):
39        # Construct a Future to return to our caller.  This allows
40        # wait_for_messages to be yielded from a coroutine even though
41        # it is not a coroutine itself.  We will set the result of the
42        # Future when results are available.
43        result_future = Future()
44        if cursor:
45            new_count = 0
46            for msg in reversed(self.cache):
47                if msg["id"] == cursor:
48                    break
49                new_count += 1
50            if new_count:
51                result_future.set_result(self.cache[-new_count:])
52                return result_future
53        self.waiters.add(result_future)
54        return result_future
55
56    def cancel_wait(self, future):
57        self.waiters.remove(future)
58        # Set an empty result to unblock any coroutines waiting.
59        future.set_result([])
60
61    def new_messages(self, messages):
62        logging.info("Sending new message to %r listeners", len(self.waiters))
63        for future in self.waiters:
64            future.set_result(messages)
65        self.waiters = set()
66        self.cache.extend(messages)
67        if len(self.cache) > self.cache_size:
68            self.cache = self.cache[-self.cache_size:]
69
70
71# Making this a non-singleton is left as an exercise for the reader.
72global_message_buffer = MessageBuffer()
73
74
75class MainHandler(tornado.web.RequestHandler):
76    def get(self):
77        self.render("index.html", messages=global_message_buffer.cache)
78
79
80class MessageNewHandler(tornado.web.RequestHandler):
81    def post(self):
82        message = {
83            "id": str(uuid.uuid4()),
84            "body": self.get_argument("body"),
85        }
86        # to_basestring is necessary for Python 3's json encoder,
87        # which doesn't accept byte strings.
88        message["html"] = tornado.escape.to_basestring(
89            self.render_string("message.html", message=message))
90        if self.get_argument("next", None):
91            self.redirect(self.get_argument("next"))
92        else:
93            self.write(message)
94        global_message_buffer.new_messages([message])
95
96
97class MessageUpdatesHandler(tornado.web.RequestHandler):
98    @gen.coroutine
99    def post(self):
100        cursor = self.get_argument("cursor", None)
101        # Save the future returned by wait_for_messages so we can cancel
102        # it in wait_for_messages
103        self.future = global_message_buffer.wait_for_messages(cursor=cursor)
104        messages = yield self.future
105        if self.request.connection.stream.closed():
106            return
107        self.write(dict(messages=messages))
108
109    def on_connection_close(self):
110        global_message_buffer.cancel_wait(self.future)
111
112
113def main():
114    parse_command_line()
115    app = tornado.web.Application(
116        [
117            (r"/", MainHandler),
118            (r"/a/message/new", MessageNewHandler),
119            (r"/a/message/updates", MessageUpdatesHandler),
120            ],
121        cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
122        template_path=os.path.join(os.path.dirname(__file__), "templates"),
123        static_path=os.path.join(os.path.dirname(__file__), "static"),
124        xsrf_cookies=True,
125        debug=options.debug,
126        )
127    app.listen(options.port)
128    tornado.ioloop.IOLoop.current().start()
129
130
131if __name__ == "__main__":
132    main()
133