Websockets are used in Precious Leads to notify the front end of new events.

Redis required

In order to use websockets, Redis must be running.

Ping Redis to verify it’s running:

redis-cli ping

Should respond with PONG.

Django Channels

Websockets are implemented using Django Channels.

With Django Channels, clients can connect to a “group”, for example Category_1.

Each connection to a group is a channel. One user can have several channels in the same group. For example, several tabs opened or several browsers opened.

Every time a message is sent to a group, it will be broadcasted to all channels of that group.

Category endpoint

In order to receive updates regarding a category, a user must connect to the following endpoint:

ws://localhost:8000/ws/category/

Required query_params:

category_id: The id of the category you want to be notified of.

token: The access token of the user.

As of January 21st, the category endpoint is the only endpoint we have.

Keep in mind:

In production we should be using wss:// instead of ws://. I have not tested this yet, but Railway should take care of it.

Permissions and authentication

The websocket endpoints require users to be authenticated and have the right permissions.

If the token is invalid or the user does not have permission to subscribe to a particular group (category in this case), the websocket connection will be refused.

Lead Users

Lead users do not currently have permission to subscribe to category groups.

This means that lead users cannot receive updates regarding new leads or new imports.

A possible solution is to implement a CategoryUser endpoint (for example, category 5 and user 7). This would separate messages by user and would allow us to notify lead users of new leads only if they belong to them.

Types of messages received

When subscribed to a category websocket, there are 2 types of messages the client can receive:

TYPE: lead_import_update

First, you will get a message when a new import has started:

{
  "message": {
    "type": "lead_import_update",
    "status": "STARTED"
    "result": "In Progress"
  }
}

Then, you will get a message when the import has finished:

{
  "message": {
    "type": "lead_import_update",
    "status": "SUCCESS"
    "result": {
      "id": 28,
      "category_id": 1,
      "submitted_by_user_id": 2,
      "celery_task_id": "7e908a71-33e1-430a-b9ed-cd5f00f4abcf",
      "state": "SUCCESS",
      "progress": "Import finished.",
      "total_leads": 6837,
      "imported_leads": 6833,
      "leads_with_errors": 4,
      "errors": [],
      "user_checked": false,
      "date_added": "2025-01-21T22:36:50.263634Z",
      "last_updated": "2025-01-21T22:36:58.173902Z"
  }
}

TYPE: lead_added

The message will be as follows:

{
  "message": {
  "type": "lead_added",
  "lead: {
      "id": 185641,
      "category_id": 1,
      "status_id": 20,
      "assigned_user_id": "4",
      "import_task_id": null,
      "source": "",
      "full_name": "Janick Gers",
      "first_name": "Janick",
      "last_name": "Gers",
      "email": "test@gmail.com",
      "phone": "740994451",
      "street_address": "Street 54nd",
      "city": "Los Angeles",
      "state": "California",
      "country": "US",
      "zip_code": "50021",
      "gender": "Male",
      "birthdate": "1985-03-11",
      "order_index": 1,
      "date_added": "2025-01-21T22:29:55.747729Z",
      "last_updated": "2025-01-21T22:29:55.758863Z",
      "custom_fields": null
  }
}

Closing channels when user permissions change

This is not implemented yet.

Quick testing with HTML file

You can quickly test websockets functionality using the following html file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebSocket Test</title>
</head>
<body>
  <h1>WebSocket Test</h1>
  <script>
    const socket = new WebSocket("ws://localhost:8000/ws/category/?category_id=1&token=jv0QIZk1u8UHfjNmPqnpEEuN48p1u7");

    socket.onopen = () => {
      console.log("WebSocket connected.");
      socket.send(JSON.stringify({ test: "Hello from client" }));
    };

    socket.onmessage = (event) => {
      console.log("Message from server:", event.data);
    };

    socket.onerror = (error) => {
      console.error("WebSocket error:", error);
    };

    socket.onclose = () => {
      console.log("WebSocket closed.");
    };
  </script>
</body>
</html>