diff --git a/demos/websocket/chatdemo.py b/demos/websocket/chatdemo.py new file mode 100755 index 00000000..dd5a48db --- /dev/null +++ b/demos/websocket/chatdemo.py @@ -0,0 +1,101 @@ +#!/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() diff --git a/demos/websocket/static/chat.css b/demos/websocket/static/chat.css new file mode 100644 index 00000000..a400c326 --- /dev/null +++ b/demos/websocket/static/chat.css @@ -0,0 +1,56 @@ +/* + * 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; +} diff --git a/demos/websocket/static/chat.js b/demos/websocket/static/chat.js new file mode 100644 index 00000000..236cb0d4 --- /dev/null +++ b/demos/websocket/static/chat.js @@ -0,0 +1,67 @@ +// 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(); + } +}; diff --git a/demos/websocket/templates/index.html b/demos/websocket/templates/index.html new file mode 100644 index 00000000..68721e7e --- /dev/null +++ b/demos/websocket/templates/index.html @@ -0,0 +1,33 @@ + + +
+ +