django-admin.py startproject myproject cd myproject /
SESSION_ENGINE = 'redis_sessions.session'
try: from local_settings import * except ImportError: pass
import os PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
STATICFILES_DIRS = ( os.path.join(PROJECT_ROOT, "static"), )
TEMPLATE_DIRS = ( os.path.join(PROJECT_ROOT, "templates"), )
API_KEY = '$0m3-U/\/1qu3-K3Y' SEND_MESSAGE_API_URL = 'http://127.0.0.1:8000/messages/send_message_api'
</ dev / urandom tr -dc _A-Zaz-0-9 | head -c $ {1: -32}; echo;
python manage.py startapp privatemessages
from django.db import models from django.db.models.signals import post_save from django.contrib.auth.models import User # Create your models here. class Thread(models.Model): participants = models.ManyToManyField(User) last_message = models.DateTimeField(null=True, blank=True, db_index=True) class Message(models.Model): text = models.TextField() sender = models.ForeignKey(User) thread = models.ForeignKey(Thread) datetime = models.DateTimeField(auto_now_add=True, db_index=True) def update_last_message_datetime(sender, instance, created, **kwargs): """ Update Thread's last_message field when a new message is sent. """ if not created: return Thread.objects.filter(id=instance.thread.id).update( last_message=instance.datetime ) post_save.connect(update_last_message_datetime, sender=Message)
python manage.py syncdb
# Create your views here. import json import redis from django.shortcuts import render_to_response, get_object_or_404 from django.http import HttpResponse, HttpResponseRedirect from django.template import RequestContext from django.core.urlresolvers import reverse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.conf import settings from django.contrib.auth.models import User from privatemessages.models import Thread, Message from privatemessages.utils import json_response, send_message def send_message_view(request): if not request.method == "POST": return HttpResponse("Please use POST.") if not request.user.is_authenticated(): return HttpResponse("Please sign in.") message_text = request.POST.get("message") if not message_text: return HttpResponse("No message found.") if len(message_text) > 10000: return HttpResponse("The message is too long.") recipient_name = request.POST.get("recipient_name") try: recipient = User.objects.get(username=recipient_name) except User.DoesNotExist: return HttpResponse("No such user.") if recipient == request.user: return HttpResponse("You cannot send messages to yourself.") thread_queryset = Thread.objects.filter( participants=recipient ).filter( participants=request.user ) if thread_queryset.exists(): thread = thread_queryset[0] else: thread = Thread.objects.create() thread.participants.add(request.user, recipient) send_message( thread.id, request.user.id, message_text, request.user.username ) return HttpResponseRedirect( reverse('privatemessages.views.messages_view') ) @csrf_exempt def send_message_api_view(request, thread_id): if not request.method == "POST": return json_response({"error": "Please use POST."}) api_key = request.POST.get("api_key") if api_key != settings.API_KEY: return json_response({"error": "Please pass a correct API key."}) try: thread = Thread.objects.get(id=thread_id) except Thread.DoesNotExist: return json_response({"error": "No such thread."}) try: sender = User.objects.get(id=request.POST.get("sender_id")) except User.DoesNotExist: return json_response({"error": "No such user."}) message_text = request.POST.get("message") if not message_text: return json_response({"error": "No message found."}) if len(message_text) > 10000: return json_response({"error": "The message is too long."}) send_message( thread.id, sender.id, message_text ) return json_response({"status": "ok"}) def messages_view(request): if not request.user.is_authenticated(): return HttpResponse("Please sign in.") threads = Thread.objects.filter( participants=request.user ).order_by("-last_message") if not threads: return render_to_response('private_messages.html', {}, context_instance=RequestContext(request)) r = redis.StrictRedis() user_id = str(request.user.id) for thread in threads: thread.partner = thread.participants.exclude(id=request.user.id)[0] thread.total_messages = r.hget( "".join(["thread_", str(thread.id), "_messages"]), "total_messages" ) return render_to_response('private_messages.html', { "threads": threads, }, context_instance=RequestContext(request)) def chat_view(request, thread_id): if not request.user.is_authenticated(): return HttpResponse("Please sign in.") thread = get_object_or_404( Thread, id=thread_id, participants__id=request.user.id ) messages = thread.message_set.order_by("-datetime")[:100] user_id = str(request.user.id) r = redis.StrictRedis() messages_total = r.hget( "".join(["thread_", thread_id, "_messages"]), "total_messages" ) messages_sent = r.hget( "".join(["thread_", thread_id, "_messages"]), "".join(["from_", user_id]) ) if messages_total: messages_total = int(messages_total) else: messages_total = 0 if messages_sent: messages_sent = int(messages_sent) else: messages_sent = 0 messages_received = messages_total-messages_sent partner = thread.participants.exclude(id=request.user.id)[0] tz = request.COOKIES.get("timezone") if tz: timezone.activate(tz) return render_to_response('chat.html', { "thread_id": thread_id, "thread_messages": messages, "messages_total": messages_total, "messages_sent": messages_sent, "messages_received": messages_received, "partner": partner, }, context_instance=RequestContext(request))
import json import redis from django.utils import dateformat from privatemessages.models import Message def json_response(obj): """ This function takes a Python object (a dictionary or a list) as an argument and returns an HttpResponse object containing the data from the object exported into the JSON format. """ return HttpResponse(json.dumps(obj), content_type="application/json") def send_message(thread_id, sender_id, message_text, sender_name=None): """ This function takes Thread object id (first argument), sender id (second argument), message text (third argument) and can also take sender's name. It creates a new Message object and increases the values stored in Redis that represent the total number of messages for the thread and the number of this thread's messages sent from this specific user. If a sender's name is passed, it also publishes the message in the thread's channel in Redis (otherwise it is assumed that the message was already published in the channel). """ message = Message() message.text = message_text message.thread_id = thread_id message.sender_id = sender_id message.save() thread_id = str(thread_id) sender_id = str(sender_id) r = redis.StrictRedis() if sender_name: r.publish("".join(["thread_", thread_id, "_messages"]), json.dumps({ "timestamp": dateformat.format(message.datetime, 'U'), "sender": sender_name, "text": message_text, })) for key in ("total_messages", "".join(["from_", sender_id])): r.hincrby( "".join(["thread_", thread_id, "_messages"]), key, 1 )
from django.conf.urls import patterns, url urlpatterns = patterns('privatemessages.views', url(r'^send_message/$', 'send_message_view'), url(r'^send_message_api/(?P<thread_id>\d+)/$', 'send_message_api_view'), url(r'^chat/(?P<thread_id>\d+)/$', 'chat_view'), url(r'^$', 'messages_view'), )
from django.conf.urls import patterns, include, url # something else urlpatterns = patterns('', # something else url(r'^messages/', include('privatemessages.urls')), # something else )
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="/static/privatemessages.css"> <script type="text/javascript" src="http://yandex.st/jquery/1.8.3/jquery.min.js"></script> <script type="text/javascript" src="/static/jstz.min.js"></script> <script type="text/javascript" src="/static/privatemessages.js"></script> {% block head %}{% endblock %} <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>{% block title %} {% endblock title %}</title> </head> <body> {% block content %}{% endblock content %} </body> </html>
{% extends "base.html" %} {% block title %}{{ partner.username }}{% endblock %} {% block head %} <script type="text/javascript"> $(document).ready(function() { activate_chat({{ thread_id }}, "{{ user.username }}", { "total": {{ messages_total }}, "sent": {{ messages_sent }}, "received": {{ messages_received }} }); }); </script> {% endblock %} {% block content %} {% load pluralize %} <div class="chat"> <div class="partner"> <p class="name">{{ partner.username }}</p> <p class="messages"><span class="total">{{ messages_total }}</span> {{ messages_total|rupluralize:",," }} (<span class="received">{{ messages_received }}</span> , <span class="sent">{{ messages_sent }}</span> )</p> </div> <div class="conversation"> {% for message in thread_messages reversed %} <div class="message"> {% if message.sender == user %}<p class="author we"><span class="datetime">{{ message.datetime|date:"dmY H:i:s" }}</span> {{ user.username }}:</p>{% else %}<p class="author partner"><span class="datetime">{{ message.datetime|date:"dmY H:i:s" }}</span> {{ partner.username }}:</p>{% endif %} <p class="message">{{ message.text|linebreaksbr }}</p> </div> {% endfor %} </div> <form class="message_form"> <div class="compose"> <textarea rows="1" cols="30" id="message_textarea"></textarea> </div> <div class="send"> <button class="btn" type="button"></button> <p> Ctrl + Enter.</p> </div> </form> </div> {% endblock content %}
{% extends "base.html" %} {% block content %} {% load pluralize %} <div class="private_messages"> <h1></h1> <div class="partners"> {% for thread in threads %} <p><a href="{% url privatemessages.views.chat_view thread.id %}">{{ thread.partner.username }} ({{ thread.total_messages|default_if_none:"0" }} {{ thread.total_messages|rupluralize:",," }})</a></p> {% empty %} <p> .</p> {% endfor %} </div> <h1> </h1> <form action="{% url privatemessages.views.send_message_view %}" method="post" class="new_message"> {% csrf_token %} <p class="name"><input name="recipient_name" placeholder=" "></p> <p><textarea name="message" placeholder=""></textarea></p> <p><input type="submit" value=""></p> </form> </div> {% endblock content %}
from django import template register = template.Library() @register.filter def rupluralize(value, arg): args = arg.split(",") try: number = abs(int(value)) except TypeError: number = 0 a = number % 10 b = number % 100 if (a == 1) and (b != 11): return args[0] elif (a >= 2) and (a <= 4) and ((b < 10) or (b >= 20)): return args[1] else: return args[2]
html, body { height: 100%; margin: 0; } body { font-family: Geneva, Arial, Helvetica, sans-serif; font-size: 14px; color: #000; background: #fff; } textarea, form p.name input { border: 1px #d4d4d4 solid; } form.new_message p { margin: 4px 0; } form.new_message p.name input, form.new_message textarea { width: 300px; } form.new_message textarea { height: 100px; } textarea:focus, input.name:focus { border-color: #cacaca; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 3px rgba(150, 150, 150, 0.5); -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 3px rgba(150, 150, 150, 0.5); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 3px rgba(150, 150, 150, 0.5); } div.private_messages { padding: 10px; } div.private_messages h1 { margin-top: 0; } div.private_messages div.partners { margin-bottom: 30px; } div.chat { height: 100%; min-height: 400px; min-width: 600px; position: relative; background-color: #e0e0e0; } div.chat div.partner { height: 50px; padding: 5px; } div.chat div.partner p { margin: 0; } div.chat div.partner p.name { font-weight: bold; margin-bottom: 3px; } div.chat div.conversation { position: absolute; top: 50px; left: 5px; right: 5px; bottom: 140px; overflow: auto; padding: 0 5px; border-style: solid; border-color: #eee; border-width: 10px 0; background-color: #fff; -webkit-border-radius: 7px; -moz-border-radius: 7px; border-radius: 7px; } div.chat div.conversation div.message { padding: 5px 0; } div.chat div.conversation div.message p { margin: 0; } div.chat div.conversation div.message p.author.partner { color: #002c64; } div.chat div.conversation div.message p.author.we { color: #216300; } div.chat div.conversation div.message p.author span.datetime { font-size: 12px; } div.chat form.message_form { position: absolute; margin: 0; left: 0; right: 0; bottom: 5px; height: 130px; } div.chat form.message_form div.outdated_browser_message { margin: 10px 5px; } div.chat form.message_form div.compose { float: left; height: 100%; width: 80%; padding: 0 5px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } div.chat form.message_form div.compose textarea { width: 100%; height: 100%; margin: 0; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; resize: none; } div.chat form.message_form div.send { float: left; height: 100%; width: 20%; min-width: 100px; padding-right: 5px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } div.chat form.message_form div.send p { margin-top: 5px; color: #333; } div.chat form.message_form div.send button { width: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } function setCookie(key, value) { document.cookie = escape(key) + '=' + escape(value); } function getNumEnding(iNumber, aEndings) { var sEnding, i; iNumber = iNumber % 100; if (iNumber>=11 && iNumber<=19) { sEnding=aEndings[2]; } else { i = iNumber % 10; switch (i) { case (1): sEnding = aEndings[0]; break; case (2): case (3): case (4): sEnding = aEndings[1]; break; default: sEnding = aEndings[2]; } } return sEnding; } var timezone = getCookie('timezone'); if (timezone == null) { setCookie("timezone", jstz.determine().name()); } function activate_chat(thread_id, user_name, number_of_messages) { $("div.chat form.message_form div.compose textarea").focus(); function scroll_chat_window() { $("div.chat div.conversation").scrollTop($("div.chat div.conversation")[0].scrollHeight); } scroll_chat_window(); var ws; function start_chat_ws() { ws = new WebSocket("ws://127.0.0.1:8888/" + thread_id + "/"); ws.onmessage = function(event) { var message_data = JSON.parse(event.data); var date = new Date(message_data.timestamp*1000); var time = $.map([date.getHours(), date.getMinutes(), date.getSeconds()], function(val, i) { return (val < 10) ? '0' + val : val; }); $("div.chat div.conversation").append('<div class="message"><p class="author ' + ((message_data.sender == user_name) ? 'we' : 'partner') + '"><span class="datetime">' + time[0] + ':' + time[1] + ':' + time[2] + '</span> ' + message_data.sender + ':</p><p class="message">' + message_data.text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g, '<br />') + '</p></div>'); scroll_chat_window(); number_of_messages["total"]++; if (message_data.sender == user_name) { number_of_messages["sent"]++; } else { number_of_messages["received"]++; } $("div.chat p.messages").html('<span class="total">' + number_of_messages["total"] + '</span> ' + getNumEnding(number_of_messages["total"], ["", "", ""]) + ' (<span class="received">' + number_of_messages["received"] + '</span> , <span class="sent">' + number_of_messages["sent"] + '</span> )'); } ws.onclose = function(){ // Try to reconnect in 5 seconds setTimeout(function() {start_chat_ws()}, 5000); }; } if ("WebSocket" in window) { start_chat_ws(); } else { $("form.message_form").html('<div class="outdated_browser_message"><p><em>!</em> . , :</p><ul><li> <em>Android</em>: <a href="http://www.mozilla.org/ru/mobile/">Firefox</a>, <a href="http://www.google.com/intl/en/chrome/browser/mobile/android.html">Google Chrome</a>, <a href="https://play.google.com/store/apps/details?id=com.opera.browser">Opera Mobile</a></li><li> <em>Linux</em>, <em>Mac OS X</em> <em>Windows</em>: <a href="http://www.mozilla.org/ru/firefox/fx/">Firefox</a>, <a href="https://www.google.com/intl/ru/chrome/browser/">Google Chrome</a>, <a href="http://ru.opera.com/browser/download/">Opera</a></li></ul></div>'); return false; } function send_message() { var textarea = $("textarea#message_textarea"); if (textarea.val() == "") { return false; } if (ws.readyState != WebSocket.OPEN) { return false; } ws.send(textarea.val()); textarea.val(""); } $("form.message_form div.send button").click(send_message); $("textarea#message_textarea").keydown(function (e) { // Ctrl + Enter if (e.ctrlKey && e.keyCode == 13) { send_message(); } }); }
import datetime import json import time import urllib import brukva import tornado.web import tornado.websocket import tornado.ioloop import tornado.httpclient from django.conf import settings from django.utils.importlib import import_module session_engine = import_module(settings.SESSION_ENGINE) from django.contrib.auth.models import User from privatemessages.models import Thread c = brukva.Client() c.connect() class MainHandler(tornado.web.RequestHandler): def get(self): self.set_header('Content-Type', 'text/plain') self.write('Hello. :)') class MessagesHandler(tornado.websocket.WebSocketHandler): def __init__(self, *args, **kwargs): super(MessagesHandler, self).__init__(*args, **kwargs) self.client = brukva.Client() self.client.connect() def open(self, thread_id): session_key = self.get_cookie(settings.SESSION_COOKIE_NAME) session = session_engine.SessionStore(session_key) try: self.user_id = session["_auth_user_id"] self.sender_name = User.objects.get(id=self.user_id).username except (KeyError, User.DoesNotExist): self.close() return if not Thread.objects.filter( id=thread_id, participants__id=self.user_id ).exists(): self.close() return self.channel = "".join(['thread_', thread_id,'_messages']) self.client.subscribe(self.channel) self.thread_id = thread_id self.client.listen(self.show_new_message) def handle_request(self, response): pass def on_message(self, message): if not message: return if len(message) > 10000: return c.publish(self.channel, json.dumps({ "timestamp": int(time.time()), "sender": self.sender_name, "text": message, })) http_client = tornado.httpclient.AsyncHTTPClient() request = tornado.httpclient.HTTPRequest( "".join([ settings.SEND_MESSAGE_API_URL, "/", self.thread_id, "/" ]), method="POST", body=urllib.urlencode({ "message": message.encode("utf-8"), "api_key": settings.API_KEY, "sender_id": self.user_id, }) ) http_client.fetch(request, self.handle_request) def show_new_message(self, result): self.write_message(str(result.body)) def on_close(self): try: self.client.unsubscribe(self.channel) except AttributeError: pass def check(): if self.client.connection.in_progress: tornado.ioloop.IOLoop.instance().add_timeout( datetime.timedelta(0.00001), check ) else: self.client.disconnect() tornado.ioloop.IOLoop.instance().add_timeout( datetime.timedelta(0.00001), check ) application = tornado.web.Application([ (r"/", MainHandler), (r'/(?P<thread_id>\d+)/', MessagesHandler), ])
Friendfeed uses mysql, with the standard synchronous MySQLdb module (http://bret.appspot.com/entry/how-friendfeed-uses-mysql). If you’re not thinking about what you need, it’s not worth it. managing callbacks. If you want to make it, it’s possible to keep the amount of time (long polling). It is not clear that it makes it possible to use it.
import signal import time import tornado.httpserver import tornado.ioloop from django.core.management.base import BaseCommand, CommandError from privatemessages.tornadoapp import application class Command(BaseCommand): args = '[port_number]' help = 'Starts the Tornado application for message handling.' def sig_handler(self, sig, frame): """Catch signal and init callback""" tornado.ioloop.IOLoop.instance().add_callback(self.shutdown) def shutdown(self): """Stop server and add callback to stop i/o loop""" self.http_server.stop() io_loop = tornado.ioloop.IOLoop.instance() io_loop.add_timeout(time.time() + 2, io_loop.stop) def handle(self, *args, **options): if len(args) == 1: try: port = int(args[0]) except ValueError: raise CommandError('Invalid port number specified') else: port = 8888 self.http_server = tornado.httpserver.HTTPServer(application) self.http_server.listen(port, address="127.0.0.1") # Init signals handler signal.signal(signal.SIGTERM, self.sig_handler) # This will also catch KeyboardInterrupt exception signal.signal(signal.SIGINT, self.sig_handler) tornado.ioloop.IOLoop.instance().start()
python manage.py runserver python manage.py starttornadoapp
[program:django] command=gunicorn_django --workers 4 -b 127.0.0.1:8150 directory=/home/yourusername/myproject user=yourusername autostart=true autorestart=true
process_name = tornado-%(process_num)s user = yourusername directory = /home/yourusername/myproject command = python manage.py starttornadoapp %(process_num)s # Increase numprocs to run multiple processes on different ports. # Note that the chat demo won't actually work in that configuration # because it assumes all listeners are in one process. numprocs = 4 numprocs_start = 8000 autostart = true autorestart = true
ssh -L 8000: localhost: 8150 someverycoolserver.com
server { listen 127.0.0.1:8100; server_name someverycoolserver.com; # no security problem here, since / is always passed to upstream root /home/yourusername/myproject/myproject/static/; ## Compression # src: http://www.ruby-forum.com/topic/141251 # src: http://wiki.brightbox.co.uk/docs:nginx gzip on; gzip_http_version 1.0; gzip_comp_level 2; gzip_proxied any; gzip_min_length 1100; gzip_buffers 16 8k; gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript; # Some version of IE 6 don't handle compression well on some mime-types, so just disable for them gzip_disable "MSIE [1-6].(?!.*SV1)"; # Set a vary header so downstream proxies don't send cached gzipped content to IE6 gzip_vary on; ## /Compression location /static/admin/ { # this changes depending on your python version root /usr/local/lib/python2.7/dist-packages/django/contrib/admin/; } location /robots.txt { alias /home/yourusername/myproject/myproject/robots.txt; } location /favicon.ico { alias /home/yourusername/myproject/myproject/img/favicon.ico; expires 3d; } location /static/ { root /home/yourusername/myproject/myproject/; expires 3d; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_connect_timeout 10; proxy_read_timeout 10; proxy_pass http://localhost:8150/; } }
global maxconn 10000 # Total Max Connections. This is dependent on ulimit nbproc 2 defaults mode http option redispatch maxconn 2000 contimeout 5000 clitimeout 50000 srvtimeout 50000 option httpclose frontend all 0.0.0.0:80 timeout client 86400000 acl is_chat hdr_beg(host) -i chat use_backend socket_backend if is_chat default_backend www_backend backend www_backend option forwardfor # This sets X-Forwarded-For timeout server 30000 timeout connect 4000 server server1 localhost:8100 backend socket_backend balance roundrobin option forwardfor # This sets X-Forwarded-For no option httpclose # To match the `Connection` header for the websocket protocol rev. 76 option http-server-close option http-pretend-keepalive timeout queue 5000 timeout server 86400000 timeout connect 86400000 server server1 localhost:8000 weight 1 maxconn 5000 check server server2 localhost:8001 weight 1 maxconn 5000 check server server2 localhost:8002 weight 1 maxconn 5000 check server server2 localhost:8003 weight 1 maxconn 5000 check
Source: https://habr.com/ru/post/160123/
All Articles