Django REST Framework - WebSocket
Tags: python, django, websocket
Published: 2022-01-21 19:47:53

In this article we will build an application which uses JWT Authentication that communicates to websocket with Django REST Framework. The main focus of this article is send data to websocket from out of consumer.

Before we start you can find the source code [here](https://github.com/buurak/django_websocket)


We have two requests to same endpoint GET and POST, whenever a request comes to endpoint we will send messages to websocket.

This is our `models.py`, our Message model looks like down below which is very simple.

```python
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Message(models.Model):
    message = models.JSONField()
    user = models.ForeignKey(to=User, on_delete=models.CASCADE, null=True, blank=True)

```



##Configurations

We need a `routing.py` file which includes url's of related app.

```python
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'msg/', consumers.ChatConsumer.as_asgi()),
]

```

Then we need another `routing.py` file into project's core directory.

```python
import os

from websocket.middlewares import WebSocketJWTAuthMiddleware
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application

from websocket import routing

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_ws.settings")


application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": WebSocketJWTAuthMiddleware(URLRouter(routing.websocket_urlpatterns)),
    }
)

```
As you can see here a middleware is used, what this middleware does is control the authentication process, if you follow this [link](https://channels.readthedocs.io/en/stable/topics/authentication.html#authentication) you will see that Django has support for standard Django authentication, since we are using JWT Authentication a custom middleware is needed. In this project [Rest Framework SimpleJWT](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/) was used, when create a connection we are sending token with a query-string which is NOT so secure. We assigned user to scope as down below. 
`middlewares.py`
```python
from urllib.parse import parse_qs

from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.tokens import AccessToken, TokenError

User = get_user_model()


@database_sync_to_async
def get_user(user_id):
    try:
        return User.objects.get(id=user_id)
    except User.DoesNotExist:
        return AnonymousUser()


class WebSocketJWTAuthMiddleware:

    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        parsed_query_string = parse_qs(scope["query_string"])
        token = parsed_query_string.get(b"token")[0].decode("utf-8")

        try:
            access_token = AccessToken(token)
            scope["user"] = await get_user(access_token["user_id"])
        except TokenError:
            scope["user"] = AnonymousUser()

        return await self.app(scope, receive, send)
```

Last but not least settings.py file, we are using Redis as channel layer therefore we need to start a Redis server, we can do that with docker

`docker run -p 6379:6379 -d redis:5`


`settings.py`
```python
ASGI_APPLICATION = 'django_ws.routing.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('0.0.0.0', 6379)],
        },
    },
}
```

## Consumers
Here is our `consumer.py` file, since ChatConsumer is asynchronous when a database access is needed, related method needs a `database_sync_to_async` decorator.

`websocket/consumer.py`


```python
import json

from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import AnonymousUser

from websocket.models import Message


class ChatConsumer(AsyncWebsocketConsumer):
    groups = ["general"]

    async def connect(self):
        await self.accept()
        if self.scope["user"] is not AnonymousUser:
            self.user_id = self.scope["user"].id
            await self.channel_layer.group_add(f"{self.user_id}-message", self.channel_name)

    async def send_info_to_user_group(self, event):
        message = event["text"]
        await self.send(text_data=json.dumps(message))

    async def send_last_message(self, event):
        last_msg = await self.get_last_message(self.user_id)
        last_msg["status"] = event["text"]
        await self.send(text_data=json.dumps(last_msg))

    @database_sync_to_async
    def get_last_message(self, user_id):
        message = Message.objects.filter(user_id=user_id).last()
        return message.message

```

##Views

As I mentioned at previous step our consumer is asynchronous we need to convert methods from async to sync just like the name of the function

`views.py`
```python
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Message


class MessageSendAPIView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            "general", {"type": "send_info_to_user_group",
                        "text": {"status": "done"}}
        )

        return Response({"status": True}, status=status.HTTP_200_OK)

    def post(self, request):
        msg = Message.objects.create(user=request.user, message={
                                     "message": request.data["message"]})
        socket_message = f"Message with id {msg.id} was created!"
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            f"{request.user.id}-message", {"type": "send_last_message",
                                           "text": socket_message}
        )

        return Response({"status": True}, status=status.HTTP_201_CREATED)

```
In this view's GET request we are sending message to channel's "general" group so everyone on this group will receive that message, if you check the `consumers.py` you will see that our default group is "general", on the other hand in POST request we are sending our message to a specified group in an other saying, this message is only sent to a group with related user's id which means only this user receives the message.

Up to this point everything seems fine but we can't find out if it actually works fine until we try, so let's do it. I am using a Chrome [extension](https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn) to connect websocket.

Result of GET request

![Result of GET request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwp06wnlza2ielb6pcbx.png)

POST request


![POST request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryzw0dt8klz67uckdrcs.png)

NOTE: Python3.10.0 have compatibility issue with asyncio be sure not using this version.