#!/usr/bin/env python # # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import tornado.auth import tornado.escape import tornado.ioloop import tornado.web import os.path import uuid from tornado import gen from tornado.options import define, options, parse_command_line define("port", default=8888, help="run on the given port", type=int) class MessageBuffer(object): def __init__(self): self.waiters = set() self.cache = [] self.cache_size = 200 def wait_for_messages(self, callback, cursor=None): if cursor: new_count = 0 for msg in reversed(self.cache): if msg["id"] == cursor: break new_count += 1 if new_count: callback(self.cache[-new_count:]) return self.waiters.add(callback) def cancel_wait(self, callback): self.waiters.remove(callback) def new_messages(self, messages): logging.info("Sending new message to %r listeners", len(self.waiters)) for callback in self.waiters: try: callback(messages) except: logging.error("Error in waiter callback", exc_info=True) self.waiters = set() self.cache.extend(messages) if len(self.cache) > self.cache_size: self.cache = self.cache[-self.cache_size:] # Making this a non-singleton is left as an exercise for the reader. global_message_buffer = MessageBuffer() class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("chatdemo_user") if not user_json: return None return tornado.escape.json_decode(user_json) class MainHandler(BaseHandler): @tornado.web.authenticated def get(self): self.render("index.html", messages=global_message_buffer.cache) class MessageNewHandler(BaseHandler): @tornado.web.authenticated def post(self): message = { "id": str(uuid.uuid4()), "from": self.current_user["first_name"], "body": self.get_argument("body"), } # to_basestring is necessary for Python 3's json encoder, # which doesn't accept byte strings. message["html"] = tornado.escape.to_basestring( self.render_string("message.html", message=message)) if self.get_argument("next", None): self.redirect(self.get_argument("next")) else: self.write(message) global_message_buffer.new_messages([message]) class MessageUpdatesHandler(BaseHandler): @tornado.web.authenticated @tornado.web.asynchronous def post(self): cursor = self.get_argument("cursor", None) global_message_buffer.wait_for_messages(self.on_new_messages, cursor=cursor) def on_new_messages(self, messages): # Closed client connection if self.request.connection.stream.closed(): return self.finish(dict(messages=messages)) def on_connection_close(self): global_message_buffer.cancel_wait(self.on_new_messages) class AuthLoginHandler(BaseHandler, tornado.auth.GoogleMixin): @tornado.web.asynchronous @gen.coroutine def get(self): if self.get_argument("openid.mode", None): user = yield self.get_authenticated_user() self.set_secure_cookie("chatdemo_user", tornado.escape.json_encode(user)) self.redirect("/") return self.authenticate_redirect(ax_attrs=["name"]) class AuthLogoutHandler(BaseHandler): def get(self): self.clear_cookie("chatdemo_user") self.write("You are now logged out") def main(): parse_command_line() app = tornado.web.Application( [ (r"/", MainHandler), (r"/auth/login", AuthLoginHandler), (r"/auth/logout", AuthLogoutHandler), (r"/a/message/new", MessageNewHandler), (r"/a/message/updates", MessageUpdatesHandler), ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", login_url="/auth/login", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, ) app.listen(options.port) tornado.ioloop.IOLoop.instance().start() if __name__ == "__main__": main()