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