Recently, I launched the site backgrounddating.com and wrote about it here on Habrahabr. Of course, I already spoke about some of the technical details of the implementation of this project, but I would like to write about one of the site’s capabilities separately, especially since the documentation (both in Russian and in English) on this topic on the Internet is still quite small. . So, it’s about real-time chat between two users. The task is to allow any user to send messages to other users, and if the recipient of the message has a chat with these users, he immediately saw the incoming messages (otherwise, he could read the messages later: that is, when the chat opens, history of recent posts).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