Full chat room example ====================== This example shows how to build a simple chat room using Django-AWS-API-Gateway-WebSockets. The example includes: * an HTML client using ``reconnecting-websocket``; * fetching historic messages after the WebSocket connects; * sending a chat message from the browser; * broadcasting messages to all users in the same room; * changing rooms by changing the WebSocket channel; * broadcasting a maintenance message to all connected users from the Django shell. Overview -------- In this example, each chat room is represented by a WebSocket ``channel``. For example: .. code-block:: text general support random When a browser connects to: .. code-block:: text wss://ws.example.com?channel=general the connection is associated with the ``general`` channel. When a user changes room, the browser closes the current WebSocket connection and opens a new one with a different ``channel`` query string value. Example assumptions ------------------- This example assumes: * the package is installed and configured; * an API Gateway WebSocket endpoint has been created; * your WebSocket endpoint is available at ``wss://ws.example.com``; * WebSocket token protection is disabled for simplicity; * your Django app is called ``chat``; * the WebSocket route selection key is ``action``; * messages sent with ``"action": "chat_message"`` are handled by the ``chat_message`` method on the view; * messages sent with ``"action": "fetch_history"`` are handled by the ``fetch_history`` method on the view; * messages sent with ``"action": "change_channel"`` are handled by the ``change_channel`` method on the view. For production deployments, you should also enable authentication, authorisation, input validation, rate limiting, and WebSocket token protection. Django model ------------ Create a simple model to store historic chat messages. .. code-block:: python # chat/models.py from django.conf import settings from django.db import models class ChatMessage(models.Model): room = models.CharField(max_length=100, db_index=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, ) username = models.CharField(max_length=150) message = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at"] def __str__(self): return f"[{self.room}] {self.username}: {self.message[:50]}" Create and apply migrations: .. code-block:: console python manage.py makemigrations chat python manage.py migrate Django WebSocket view --------------------- Create a WebSocket view that can: * send historic messages to the current connection; * receive a chat message; * store the message; * broadcast the message to all active connections in the same channel. .. code-block:: python # chat/views.py from django.http import JsonResponse from django.utils import timezone from django_aws_api_gateway_websockets.models import WebSocketSession from django_aws_api_gateway_websockets.views import WebSocketView from .models import ChatMessage class ChatWebSocketView(WebSocketView): def fetch_history(self, request, *args, **kwargs): room = self.websocket_session.channel_name messages = ChatMessage.objects.filter(room=room).order_by("-created_at")[:50] messages = reversed(messages) self.websocket_session.send_message( { "type": "history", "room": room, "messages": [ { "username": message.username, "message": message.message, "created_at": message.created_at.isoformat(), } for message in messages ], } ) return JsonResponse({"ok": True}) def change_channel(self, request, *args, **kwargs): super().change_channel(request, *args, **kwargs) return JsonResponse( { "ok": True, "type": "channel_changed", "channel": self.websocket_session.channel_name, } ) def chat_message(self, request, *args, **kwargs): room = self.websocket_session.channel_name message_text = str(self.body.get("message", "")).strip() if not message_text: return JsonResponse( { "ok": False, "error": "Message cannot be empty.", }, status=400, ) user = request.user if request.user.is_authenticated else None username = user.get_username() if user else "Anonymous" message = ChatMessage.objects.create( room=room, user=user, username=username, message=message_text, ) WebSocketSession.objects.filter(channel_name=room).send_message( { "type": "chat_message", "room": room, "username": message.username, "message": message.message, "created_at": message.created_at.isoformat(), } ) return JsonResponse({"ok": True}) def default(self, request, *args, **kwargs): return JsonResponse( { "ok": False, "error": "Unknown WebSocket action.", }, status=400, ) URL route --------- Add a URL pattern for API Gateway to call. The slug parameter must be named ``route``. .. code-block:: python # chat/urls.py from django.urls import path from .views import ChatWebSocketView urlpatterns = [ path( "ws/chat/", ChatWebSocketView.as_view(), name="chat_websocket", ), ] Include the app URLs from your project URL configuration if required. .. code-block:: python # project/urls.py from django.urls import include from django.urls import path urlpatterns = [ path("", include("chat.urls")), ] API Gateway target endpoint --------------------------- When creating the API Gateway record, set the target base endpoint to the URL without the ``route`` slug. For example, if your Django route is: .. code-block:: text https://www.example.com/ws/chat/ then the API Gateway target base endpoint should be: .. code-block:: text https://www.example.com/ws/chat/ The package appends the route value when configuring API Gateway. HTML client ----------- The following HTML page connects to a chat room, fetches historic messages when the connection opens, sends messages, and allows the user to change rooms. .. code-block:: html WebSocket chat room example

Chat room

Current room: general


How fetching history works -------------------------- When the WebSocket connection opens, the browser sends: .. code-block:: javascript sendMessage("fetch_history", {}); This produces a JSON message like: .. code-block:: json { "action": "fetch_history" } API Gateway routes the request to Django. The Django view then calls the ``fetch_history`` method, loads the most recent messages for the current channel, and sends them back to only the current WebSocket session. How broadcasting to the current room works ------------------------------------------ When a user sends a chat message, the browser sends: .. code-block:: javascript sendMessage("chat_message", { message: "Hello everyone" }); The Django view stores the message and then sends it to every active ``WebSocketSession`` in the same channel. .. code-block:: python WebSocketSession.objects.filter(channel_name=room).send_message( { "type": "chat_message", "room": room, "username": message.username, "message": message.message, "created_at": message.created_at.isoformat(), } ) Because the queryset is filtered by ``channel_name``, only users in the same room receive the message. How changing rooms works ------------------------ A connection is associated with a WebSocket channel. There are two common ways to change room: * close the current WebSocket connection and open a new one with a different ``channel`` query string value; * keep the current WebSocket connection open and update the stored ``WebSocketSession.channel_name`` using a WebSocket message. The ``change_channel`` handler above uses the second approach. The browser can send: .. code-block:: javascript sendMessage("change_channel", { channel: "support" }); This produces a JSON message like: .. code-block:: json { "action": "change_channel", "channel": "support" } The Django view updates the current ``WebSocketSession``: .. code-block:: python def change_channel(self, request, *args, **kwargs): self.websocket_session.channel_name = self.body["channel"] self.websocket_session.save() return JsonResponse( { "ok": True, "type": "channel_changed", "channel": self.websocket_session.channel_name, } ) After this, messages broadcast to the new channel will include this connection. For example, if the connection was originally in: .. code-block:: text general and the client sends: .. code-block:: json { "action": "change_channel", "channel": "support" } then future broadcasts to ``support`` will include this connection, and future broadcasts to ``general`` will not. If WebSocket token protection is enabled and your application opens a new WebSocket connection when changing room, request a fresh token before opening the new connection. If you use ``change_channel`` on the existing connection, a new WebSocket token is not required because the connection is not reopened. Broadcasting to all channels from the Django shell -------------------------------------------------- A systems administrator can send a message to every active WebSocket session from the Django shell. Open the shell: .. code-block:: console python manage.py shell Then run: .. code-block:: python from django_aws_api_gateway_websockets.models import WebSocketSession WebSocketSession.objects.filter(connected=True).send_message( { "type": "system", "message": "Maintenance will be performed in 5 minutes.", } ) This sends the message to every active connection, regardless of channel. Broadcasting to one room from the Django shell ---------------------------------------------- To send a message to only one room, filter by ``channel_name``: .. code-block:: python from django_aws_api_gateway_websockets.models import WebSocketSession WebSocketSession.objects.filter( connected=True, channel_name="general", ).send_message( { "type": "system", "message": "The general room will be restarted shortly.", } ) Using WebSocket tokens ---------------------- The HTML example above keeps the WebSocket connection simple by omitting WebSocket token handling. If WebSocket token protection is enabled, fetch a token before connecting and include it in the WebSocket URL. For example: .. code-block:: text wss://ws.example.com?ws_token=&channel=general When reconnecting or changing rooms, request a fresh token before opening the new WebSocket connection. Security notes -------------- For production use: * require authenticated users where appropriate; * validate message length and content; * check permissions before allowing users to join restricted rooms; * avoid trusting the room name without validation; * rate limit message sending; * use WebSocket token protection for authenticated sessions; * escape or sanitise rendered message content; * schedule cleanup for stale WebSocket sessions. Related pages ------------- See also: * :doc:`api_gateway_setup`; * :doc:`reconnecting_websocket`; * :doc:`adding_new_routes`; * :doc:`cleanup`.