1#!/usr/bin/env python3
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 asyncio
18import tornado.escape
19import tornado.ioloop
20import tornado.locks
21import tornado.web
22import os.path
23import uuid
24
25from tornado.options import define, options, parse_command_line
26
27define("port", default=8888, help="run on the given port", type=int)
28define("debug", default=True, help="run in debug mode")
29
30
31class MessageBuffer(object):
32    def __init__(self):
33        # cond is notified whenever the message cache is updated
34        self.cond = tornado.locks.Condition()
35        self.cache = []
36        self.cache_size = 200
37
38    def get_messages_since(self, cursor):
39        """Returns a list of messages newer than the given cursor.
40
41        ``cursor`` should be the ``id`` of the last message received.
42        """
43        results = []
44        for msg in reversed(self.cache):
45            if msg["id"] == cursor:
46                break
47            results.append(msg)
48        results.reverse()
49        return results
50
51    def add_message(self, message):
52        self.cache.append(message)
53        if len(self.cache) > self.cache_size:
54            self.cache = self.cache[-self.cache_size:]
55        self.cond.notify_all()
56
57
58# Making this a non-singleton is left as an exercise for the reader.
59global_message_buffer = MessageBuffer()
60
61
62class MainHandler(tornado.web.RequestHandler):
63    def get(self):
64        self.render("index.html", messages=global_message_buffer.cache)
65
66
67class MessageNewHandler(tornado.web.RequestHandler):
68    """Post a new message to the chat room."""
69    def post(self):
70        message = {
71            "id": str(uuid.uuid4()),
72            "body": self.get_argument("body"),
73        }
74        # render_string() returns a byte string, which is not supported
75        # in json, so we must convert it to a character string.
76        message["html"] = tornado.escape.to_unicode(
77            self.render_string("message.html", message=message))
78        if self.get_argument("next", None):
79            self.redirect(self.get_argument("next"))
80        else:
81            self.write(message)
82        global_message_buffer.add_message(message)
83
84
85class MessageUpdatesHandler(tornado.web.RequestHandler):
86    """Long-polling request for new messages.
87
88    Waits until new messages are available before returning anything.
89    """
90    async def post(self):
91        cursor = self.get_argument("cursor", None)
92        messages = global_message_buffer.get_messages_since(cursor)
93        while not messages:
94            # Save the Future returned here so we can cancel it in
95            # on_connection_close.
96            self.wait_future = global_message_buffer.cond.wait()
97            try:
98                await self.wait_future
99            except asyncio.CancelledError:
100                return
101            messages = global_message_buffer.get_messages_since(cursor)
102        if self.request.connection.stream.closed():
103            return
104        self.write(dict(messages=messages))
105
106    def on_connection_close(self):
107        self.wait_future.cancel()
108
109
110def main():
111    parse_command_line()
112    app = tornado.web.Application(
113        [
114            (r"/", MainHandler),
115            (r"/a/message/new", MessageNewHandler),
116            (r"/a/message/updates", MessageUpdatesHandler),
117        ],
118        cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
119        template_path=os.path.join(os.path.dirname(__file__), "templates"),
120        static_path=os.path.join(os.path.dirname(__file__), "static"),
121        xsrf_cookies=True,
122        debug=options.debug,
123    )
124    app.listen(options.port)
125    tornado.ioloop.IOLoop.current().start()
126
127
128if __name__ == "__main__":
129    main()
130