--- /dev/null
+#!/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.
+"""Simplified chat demo for websockets.
+
+Authentication, error handling, etc are left as an exercise for the reader :)
+"""
+
+import logging
+import tornado.escape
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import tornado.websocket
+import os.path
+import uuid
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/chatsocket", ChatSocketHandler),
+ ]
+ settings = dict(
+ cookie_secret="43oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ xsrf_cookies=True,
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+
+class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.render("index.html", messages=ChatSocketHandler.cache)
+
+class ChatSocketHandler(tornado.websocket.WebSocketHandler):
+ waiters = set()
+ cache = []
+ cache_size = 200
+
+ def open(self):
+ ChatSocketHandler.waiters.add(self)
+
+ def on_close(self):
+ ChatSocketHandler.waiters.remove(self)
+
+ @classmethod
+ def update_cache(cls, chat):
+ cls.cache.append(chat)
+ if len(cls.cache) > cls.cache_size:
+ cls.cache = cls.cache[-cls.cache_size:]
+
+ @classmethod
+ def send_updates(cls, chat):
+ logging.info("sending message to %d waiters", len(cls.waiters))
+ for waiter in cls.waiters:
+ try:
+ waiter.write_message(chat)
+ except:
+ logging.error("Error sending message", exc_info=True)
+
+ def on_message(self, message):
+ logging.info("got message %r", message)
+ parsed = tornado.escape.json_decode(message)
+ chat = {
+ "id": str(uuid.uuid4()),
+ "body": parsed["body"],
+ }
+ chat["html"] = self.render_string("message.html", message=chat)
+
+ ChatSocketHandler.update_cache(chat)
+ ChatSocketHandler.send_updates(chat)
+
+
+def main():
+ tornado.options.parse_command_line()
+ app = Application()
+ app.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+/*
+ * Copyright 2009 FriendFeed
+ *
+ * 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.
+ */
+
+body {
+ background: white;
+ margin: 10px;
+}
+
+body,
+input {
+ font-family: sans-serif;
+ font-size: 10pt;
+ color: black;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+#body {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+}
+
+#input {
+ margin-top: 0.5em;
+}
+
+#inbox .message {
+ padding-top: 0.25em;
+}
+
+#nav {
+ float: right;
+ z-index: 99;
+}
--- /dev/null
+// Copyright 2009 FriendFeed
+//
+// 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.
+
+$(document).ready(function() {
+ if (!window.console) window.console = {};
+ if (!window.console.log) window.console.log = function() {};
+
+ $("#messageform").live("submit", function() {
+ newMessage($(this));
+ return false;
+ });
+ $("#messageform").live("keypress", function(e) {
+ if (e.keyCode == 13) {
+ newMessage($(this));
+ return false;
+ }
+ });
+ $("#message").select();
+ updater.start();
+});
+
+function newMessage(form) {
+ var message = form.formToDict();
+ updater.socket.send(JSON.stringify(message));
+ form.find("input[type=text]").val("").select();
+}
+
+jQuery.fn.formToDict = function() {
+ var fields = this.serializeArray();
+ var json = {}
+ for (var i = 0; i < fields.length; i++) {
+ json[fields[i].name] = fields[i].value;
+ }
+ if (json.next) delete json.next;
+ return json;
+};
+
+var updater = {
+ socket: null,
+
+ start: function() {
+ updater.socket = new WebSocket("ws://localhost:8888/chatsocket");
+ updater.socket.onmessage = function(event) {
+ updater.showMessage(JSON.parse(event.data));
+ }
+ },
+
+ showMessage: function(message) {
+ var existing = $("#m" + message.id);
+ if (existing.length > 0) return;
+ var node = $(message.html);
+ node.hide();
+ $("#inbox").append(node);
+ node.slideDown();
+ }
+};
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Chat Demo</title>
+ <link rel="stylesheet" href="{{ static_url("chat.css") }}" type="text/css"/>
+ </head>
+ <body>
+ <div id="body">
+ <div id="inbox">
+ {% for message in messages %}
+ {% include "message.html" %}
+ {% end %}
+ </div>
+ <div id="input">
+ <form action="/a/message/new" method="post" id="messageform">
+ <table>
+ <tr>
+ <td><input name="body" id="message" style="width:500px"/></td>
+ <td style="padding-left:5px">
+ <input type="submit" value="{{ _("Post") }}"/>
+ <input type="hidden" name="next" value="{{ request.path }}"/>
+ {{ xsrf_form_html() }}
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+ </div>
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
+ <script src="{{ static_url("chat.js") }}" type="text/javascript"></script>
+ </body>
+</html>
--- /dev/null
+{% import tornado.escape %}
+<div class="message" id="m{{ message["id"] }}">{{ tornado.escape.linkify(message["body"]) }}</div>