Today, we will make a read receipt framework for your chat app with Django and Pusher.
First, we need to install the Python Django library if we don't already have it. To install Django, we run:
pip install django
After installing Django, it’s time to create our project. Open up a terminal, and create a new project using the following command:
django-admin startproject pusher_message
In the above command, we created a new project called pusher_message
. The next step will be to create an app inside our new project. To do that, let’s run the following commands:
1//change directory into the pusher_message directory 2 cd pusher_message 3 //create a new app where all our logic would live 4 django-admin startapp message
Once we are done setting up the new app, we need to tell Django about our new application, so we will go into our pusher_message\settings.py
and add the message app to our installed apps as seen below:
1INSTALLED_APPS = [ 2 'django.contrib.admin', 3 'django.contrib.auth', 4 'django.contrib.contenttypes', 5 'django.contrib.sessions', 6 'django.contrib.messages', 7 'django.contrib.staticfiles', 8 'message' 9 ]
After doing the above, it’s time for us to run the application and see if all went well. In our terminal shell, we run:
python manage.py runserver
If we navigate our browser to http://localhost:8000
, we should see the following:
At this point, Django is ready and set up. We now need to set up Pusher, as well as grab our app credentials. We need to sign up with Pusher and create a new app, and also copy our secret, application key and application id.
The next step is to install the required libraries:
pip install pusher
In the above bash command, we installed one package, pusher
. This is the official Pusher library for Python, which we will be using to trigger and send our messages to Pusher.
First, let us create a model class, which will generate our database structure.
Let's open up message\models.py
and replace the content with the following:
1from django.db import models 2 3 from django.contrib.auth.models import User 4 # Create your models here. 5 class Conversation(models.Model): 6 user = models.ForeignKey(User, on_delete=models.CASCADE) 7 message = models.CharField(blank=True, null=True, max_length=225) 8 status = models.CharField(blank=True, null=True, max_length=225) 9 created_at = models.DateTimeField(auto_now=True)
In the above block of code, we defined a model called Conversation
. The conversation table consists of the following fields:
We need to make migrations and also run them, so our database table can be created. To do that, let us run the following in our terminal:
1python manage.py makemigrations 2 3 python manage.py migrate
In Django, the views do not necessarily refer to the HTML structure of our application. In fact, we can see it as our Controller
as referred to in some other frameworks.
Let us open up our views.py
in our message
folder and replace the content with the following:
1from django.shortcuts import render 2 from django.contrib.auth.decorators import login_required 3 from django.views.decorators.csrf import csrf_exempt 4 from pusher import Pusher 5 from .models import * 6 from django.http import JsonResponse, HttpResponse 7 8 # instantiate pusher 9 pusher = Pusher(app_id=u'XXX_APP_ID', key=u'XXX_APP_KEY', secret=u'XXX_APP_SECRET', cluster=u'XXX_APP_CLUSTER') 10 # Create your views here. 11 #add the login required decorator, so the method cannot be accessed withour login 12 @login_required(login_url='login/') 13 def index(request): 14 return render(request,"chat.html"); 15 16 #use the csrf_exempt decorator to exempt this function from csrf checks 17 @csrf_exempt 18 def broadcast(request): 19 # collect the message from the post parameters, and save to the database 20 message = Conversation(message=request.POST.get('message', ''), status='', user=request.user); 21 message.save(); 22 # create an dictionary from the message instance so we can send only required details to pusher 23 message = {'name': message.user.username, 'status': message.status, 'message': message.message, 'id': message.id} 24 #trigger the message, channel and event to pusher 25 pusher.trigger(u'a_channel', u'an_event', message) 26 # return a json response of the broadcasted message 27 return JsonResponse(message, safe=False) 28 29 #return all conversations in the database 30 def conversations(request): 31 data = Conversation.objects.all() 32 # loop through the data and create a new list from them. Alternatively, we can serialize the whole object and send the serialized response 33 data = [{'name': person.user.username, 'status': person.status, 'message': person.message, 'id': person.id} for person in data] 34 # return a json response of the broadcasted messgae 35 return JsonResponse(data, safe=False) 36 37 #use the csrf_exempt decorator to exempt this function from csrf checks 38 @csrf_exempt 39 def delivered(request, id): 40 41 message = Conversation.objects.get(pk=id); 42 # verify it is not the same user who sent the message that wants to trigger a delivered event 43 if request.user.id != message.user.id: 44 socket_id = request.POST.get('socket_id', '') 45 message.status = 'Delivered'; 46 message.save(); 47 message = {'name': message.user.username, 'status': message.status, 'message': message.message, 'id': message.id} 48 pusher.trigger(u'a_channel', u'delivered_message', message, socket_id) 49 return HttpResponse('ok'); 50 else: 51 return HttpResponse('Awaiting Delivery');
In the code above, we have defined four main functions which are:
index
broadcast
conversation
delivered
In the index
function, we added the login required decorator, and we also passed the login URL argument which does not exist yet, as we will need to create it in the urls.py
file. Also, we rendered a default template called chat.html
which we will also create soon.
In the broadcast
function, we retrieved the content of the message being sent, saved it into our database, we finally trigger a Pusher request passing in our message dictionary, as well as a channel and event name.
In the conversations
function, we simply grab all conversations and return them as a JSON response
Finally, we have the delivered
function, which is the function which takes care of our read receipt feature.
In this function, we get the conversation by the ID supplied to us, we then verify that the user who wants to trigger the delivered event isn’t the user who sent the message in the first place. Also, we pass in the socket_id
so that Pusher does not broadcast the event back to the person who triggered it.
The socket_id
stands as an identifier for the socket connection that triggered the event.
Let us open up our pusher_message\urls.py
file and replace with the following:
1"""pusher_message URL Configuration 2 3 The `urlpatterns` list routes URLs to views. For more information please see: 4 https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 Examples: 6 Function views 7 1. Add an import: from my_app import views 8 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 Class-based views 10 1. Add an import: from other_app.views import Home 11 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 Including another URLconf 13 1. Import the include() function: from django.conf.urls import url, include 14 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 """ 16 from django.conf.urls import url 17 from django.contrib import admin 18 from django.contrib.auth import views 19 from message.views import * 20 21 urlpatterns = [ 22 url(r'^$', index), 23 url(r'^admin/', admin.site.urls), 24 url(r'^login/$', views.login, {'template_name': 'login.html'}), 25 url(r'^logout/$', views.logout, {'next_page': '/login'}), 26 url(r'^conversation$', broadcast), 27 url(r'^conversations/$', conversations), 28 url(r'^conversations/(?P<id>[-\w]+)/delivered$',delivered) 29 ]
What has changed in this file? We have added 6 new routes to the file.
We have defined the entry point, and have assigned it to our index
function. Next, we defined the login URL, which the login_required
decorator would try to access to authenticate users. We have used the default auth
function to handle it but passed in our own custom template for login, which we will create soon.
Next, we defined the routes for the conversation
message trigger, all conversations
, and finally the delivered
conversation.
Now we will need to create two HTML pages, so our application can run smoothly. We have referenced two HTML pages in the course of building the application which are:
Let us create a new folder in our messages
folder called templates
.
Next, we create a file called login.html
in our templates
folder and replace it with the following:
1<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> 2 {% if form.errors %} 3 4 <center><p>Your username and password didn't match. Please try again.</p></center> 5 {% endif %} 6 7 {% if next %} 8 {% if user.is_authenticated %} 9 10 <center><p>Your account doesn't have access to this page. To proceed, 11 please login with an account that has access.</p></center> 12 {% else %} 13 14 <center><p>Please login to see this page.</p></center> 15 {% endif %} 16 {% endif %} 17 18 <div class="container"> 19 <div class="row"> 20 <div class="col-md-4 col-md-offset-4"> 21 <div class="login-panel panel panel-default"> 22 <div class="panel-heading"> 23 <h3 class="panel-title">Please Sign In</h3> 24 </div> 25 <div class="panel-body"> 26 <form method="post" action=""> 27 {% csrf_token %} 28 29 <p class="bs-component"> 30 <table> 31 <tr> 32 <td>{{ form.username.label_tag }}</td> 33 <td>{{ form.username }}</td> 34 </tr> 35 <tr> 36 <td>{{ form.password.label_tag }}</td> 37 <td>{{ form.password }}</td> 38 </tr> 39 </table> 40 </p> 41 <p class="bs-component"> 42 <center> 43 <input class="btn btn-success btn-sm" type="submit" value="login" /> 44 </center> 45 </p> 46 <input type="hidden" name="next" value="{{ next }}" /> 47 </form> 48 </div> 49 </div> 50 </div> 51 </div> 52 </div> 53 54Next, let us create the `chat.html` file and replace it with the following: 55 56 <html> 57 <head> 58 <title> 59 </title> 60 </head> 61 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/> 62 <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.2/vue.js"></script> 63 <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"></script> 64 <script src="//js.pusher.com/4.0/pusher.min.js"></script> 65 <style> 66 .chat 67 { 68 list-style: none; 69 margin: 0; 70 padding: 0; 71 } 72 73 .chat li 74 { 75 margin-bottom: 10px; 76 padding-bottom: 5px; 77 border-bottom: 1px dotted #B3A9A9; 78 } 79 80 .chat li.left .chat-body 81 { 82 margin-left: 60px; 83 } 84 85 .chat li.right .chat-body 86 { 87 margin-right: 60px; 88 } 89 90 .chat li .chat-body p 91 { 92 margin: 0; 93 color: #777777; 94 } 95 96 .panel .slidedown .glyphicon, .chat .glyphicon 97 { 98 margin-right: 5px; 99 } 100 101 .panel-body 102 { 103 overflow-y: scroll; 104 height: 250px; 105 } 106 107 ::-webkit-scrollbar-track 108 { 109 -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 110 background-color: #F5F5F5; 111 } 112 113 ::-webkit-scrollbar 114 { 115 width: 12px; 116 background-color: #F5F5F5; 117 } 118 119 ::-webkit-scrollbar-thumb 120 { 121 -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 122 background-color: #555; 123 } 124 125 </style> 126 <body> 127 <div class="container" id="app"> 128 <div class="row"> 129 <div class="col-md-12"> 130 <div class="panel panel-primary"> 131 <div class="panel-heading"> 132 <span class="glyphicon glyphicon-comment"></span> Chat 133 134 </div> 135 <div class="panel-body"> 136 <ul class="chat" id="chat" > 137 <li class="left clearfix" v-for="data in conversations"> 138 <span class="chat-img pull-left" > 139 <img :src="'http://placehold.it/50/55C1E7/fff&text='+data.name" alt="User Avatar" class="img-circle"/> 140 </span> 141 <div class="chat-body clearfix"> 142 <div class="header"> 143 <strong class="primary-font" v-html="data.name"> </strong> <small class="pull-right text-muted" v-html="data.status"></small> 144 </div> 145 <p v-html="data.message"> 146 147 </p> 148 </div> 149 </li> 150 </ul> 151 </div> 152 <div class="panel-footer"> 153 <div class="input-group"> 154 <input id="btn-input" v-model="message" class="form-control input-sm" placeholder="Type your message here..." type="text"> 155 <span class="input-group-btn"> 156 <button class="btn btn-warning btn-sm" id="btn-chat" @click="sendMessage()"> 157 Send</button> 158 </span> 159 </div> 160 </div> 161 </div> 162 </div> 163 </div> 164 </div> 165 </body> 166 </html>
That’s it! Now, whenever a new message is delivered, it will be broadcast and we can listen using our channel to update the status in realtime.
Below is our Example component written using Vue.js
Please note: In the Vue component below, a new function called **queryParams**
was defined to serialize our POST body so it can be sent as x-www-form-urlencoded
to the server in place of as a payload
. We did this because Django cannot handle requests coming in as** payload
.
1<script> 2 var pusher = new Pusher('XXX_APP_KEY',{ 3 cluster: 'XXX_APP_CLUSTER' 4 }); 5 var socketId = null; 6 pusher.connection.bind('connected', function() { 7 socketId = pusher.connection.socket_id; 8 9 }); 10 11 var my_channel = pusher.subscribe('a_channel'); 12 var config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; 13 new Vue({ 14 el: "#app", 15 data: { 16 'message': '', 17 'conversations': [] 18 }, 19 mounted() { 20 this.getConversations(); 21 this.listen(); 22 23 }, 24 methods: { 25 sendMessage() { 26 axios.post('/conversation', this.queryParams({message: this.message}), config) 27 .then(response => { 28 this.message = ''; 29 }); 30 }, 31 getConversations() { 32 axios.get('/conversations').then((response) => { 33 this.conversations = response.data; 34 this.readall(); 35 }); 36 }, 37 listen() { 38 my_channel.bind("an_event", (data)=> { 39 this.conversations.push(data); 40 axios.post('/conversations/'+ data.id +'/delivered', this.queryParams({socket_id: socketId})); 41 }) 42 43 my_channel.bind("delivered_message", (data)=> { 44 for(var i=0; i < this.conversations.length; i++){ 45 if (this.conversations[i].id == data.id){ 46 this.conversations[i].status = data.status; 47 } 48 } 49 50 }) 51 }, 52 readall(){ 53 54 for(var i=0; i < this.conversations.length; i++){ 55 if(this.conversations[i].status=='Sent'){ 56 axios.post('/conversations/'+ this.conversations[i].id +'/delivered'); 57 } 58 } 59 60 }, 61 queryParams(source) { 62 var array = []; 63 64 for(var key in source) { 65 array.push(encodeURIComponent(key) + "=" + encodeURIComponent(source[key])); 66 } 67 68 return array.join("&"); 69 } 70 } 71 }); 72 </script>
Below is the image demonstrating what we have built:
In this article, we have covered how to create a read receipt framework using Django and Pusher. We have gone through exempting certain functions from CSRF checks, as well as exempting the broadcaster from receiving an event they triggered.