https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-social-crm-with-django-and-the-vonage-messages-api/social-crm_django.png

Creación de un CRM social con Django y la API Messages API de Vonage

Publicado el January 4, 2022

Tiempo de lectura: 11 minutos

En este artículo, aprenderás a crear la función principal de un CRM social utilizando Django y Vonage Messages API. Nuestro CRM social ayudará a los agentes de ventas y al equipo de atención al cliente a comunicarse con clientes potenciales directamente en Facebook en tiempo real. Llamémoslo Sales Fox.

Requisitos previos

  1. Crea una aplicación de mensajes desde tu panel de Vonage. Sigue los pasos descritos aquí.

  2. Autoriza a Vonage a acceder a tu página de empresa de Facebook y vincula tu aplicación a tu página de Facebook. Sigue los pasos descritos aquí.

  3. Instale Redis - Si utiliza Linux o Mac, siga las instrucciones aquí. Si utiliza Windows, siga las instrucciones aquí.

  4. Instala Ngrok. Ir a la Página de descarga de Ngrok y sigue las instrucciones para instalar Ngrok en tu ordenador.

Ahora que ha completado los requisitos previos. Es necesario configurar el entorno de desarrollo para el tutorial.

Puesta en marcha del proyecto

  1. Cree y active su entorno virtual

    Crea un directorio para tu proyecto y cambia tu directorio de trabajo al directorio que acabas de crear. A continuación, ejecuta los siguientes comandos para crear y activar un entorno virtual para tu proyecto.

    python3 -m venv sales-env

    source sales-env/bin/activate

  2. Instalar los paquetes necesarios

    Para instalar todos los paquetes necesarios a la vez, cree un archivo requirements.txt en el directorio creado en el paso 1. Copie y pegue el siguiente fragmento de código en el archivo requirements.txt archivo.

    aioredis==1.3.1
    asgiref==3.3.4
    async-timeout==3.0.1
    attrs==21.2.0
    autobahn==21.3.1
    Automat==20.2.0
    certifi==2021.10.8
    cffi==1.14.6
    channels==2.4.0
    channels-redis==2.4.2
    charset-normalizer==2.0.7
    constantly==15.1.0
    cryptography==3.4.7
    daphne==2.5.0
    Django==3.2.2
    djangorestframework==3.12.4
    hiredis==2.0.0
    hyperlink==21.0.0
    idna==3.2
    incremental==21.3.0
    msgpack==0.6.2
    Pillow==8.2.0
    pyasn1==0.4.8
    pyasn1-modules==0.2.8
    pycparser==2.20
    pyOpenSSL==20.0.1
    python-dotenv==0.19.2
    pytz==2021.1
    requests==2.26.0
    service-identity==21.1.0
    six==1.16.0
    sqlparse==0.4.1
    Twisted==21.7.0
    txaio==21.2.1
    typing-extensions==3.10.0.0
    urllib3==1.26.7
    zope.interface==5.4.0

    Ahora, instala todos los paquetes en requirements.txt ejecutando el siguiente comando en tu terminal.

    pip install -r requirements.txt

  3. Cree su proyecto Django

  • Ejecute django-admin startproject sales_fox lo siguiente para crear el proyecto Django llamado "sales_fox".

  • Crearemos dos aplicaciones en sales_fox: La aplicación lead_manager para gestionar clientes potenciales y la aplicación conversation para que los agentes de ventas se comuniquen con clientes potenciales (conocidos como leads). Ahora, vamos a crear nuestras dos aplicaciones mediante la ejecución de estos comandos.

    python manage.py startapp lead_manager
    python manage.py startapp conversation

Tenga en cuenta que en este tutorial,

  • Utilizaré las palabras "clientes potenciales" y "clientes" indistintamente. Los leads son clientes potenciales, así que no está de más considerarlos clientes cuando sea conveniente.

  • Utilizaré el término Project Directory para referirme al directorio donde tienes settings.py. Este directorio se creó cuando ejecutó django-admin startproject sales_fox.

  • Utilizaré el término Overall Directory para referirme al directorio que creaste al principio del tutorial. Contiene la carpeta de tu entorno virtual, los directorios de las aplicaciones y el directorio de tu proyecto.

  1. Preparemos SalesFox para usar Vonage.

  • Cree un .env archivo en su directorio general. Defina FACEBOOK_ID, VONAGE_API_KEYy VONAGE_API_SECRET. Su archivo .env debería tener el siguiente aspecto:

    FACEBOOK_ID=YOUR-LINKED-FACEBOOK-ID
    VONAGE_API_KEY=YOUR-VONAGE-API-KEY
    VONAGE_API_SECRET=YOUR-VONAGE-API-SECRET

    Puedes encontrar tu clave y secreto de API de Vonage en tu página de configuración de Vonage. Y puedes encontrar tu ID de Facebook en la Link social channels en la página de tu aplicación.

    En el directorio de su proyecto, vaya a settings.pycargue las variables en su archivo .env utilizando python-dotenv instalado desde requirements.txt. Añada el siguiente fragmento en settings.py para cargar el archivo .env:

    from  dotenv  import  load_dotenv	
    import  os
    load_dotenv()

load_dotenv carga todas las variables en nuestro archivo .env como variable de entorno. Ahora, define FACEBOOK_ID, VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_MESSAGES_ENDPOINT en tu archivo settings.py archivo. Sólo tiene que copiar y pegar el siguiente fragmento.

FACEBOOK_ID = os.getenv("FACEBOOK_ID")
VONAGE_API_KEY = os.getenv("VONAGE_API_KEY")
VONAGE_API_SECRET = os.getenv("VONAGE_API_SECRET")
VONAGE_MESSAGES_ENDPOINT = "https://api.nexmo.com/v0.1/messages"
  1. Configurar archivos estáticos

En settings.pyencuentra STATIC_URL y suma las variables STATICFILES_DIRS y STATIC_FILES debajo de STATIC_URLDebería tener algo como

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'

Vaya a su directorio general y cree una carpeta llamada static. Aquí es donde guardará todos sus archivos estáticos. Tenga en cuenta que sólo debe hacer esto para un entorno de desarrollo. En un entorno de producción, debe configurar un almacén externo como un cubo de AWS S3 para servir sus archivos estáticos.

  1. Actualizar las aplicaciones instaladas y definir la capa de canales

Tenemos que añadir channels y las aplicaciones que hemos creado (lead_manager y conversation) a INSTALLED_APPS en el directorio settings.py. Su INSTALLED_APPS en el archivo settings.py debe tener este aspecto:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'lead_manager',
    'conversation',
]

Los canales Django nos ayudan a incluir soporte WebSocket a Sales Fox. Una capa de canales introduce el uso de canales y grupos en SalesFox. Nos ayuda a construir características distribuidas en nuestra aplicación. Puedes leer más sobre capas de canales aquí. Para este proyecto, usaré Redis como nuestra capa de canal. Hemos instalado channels-redis desde requirements.txt. Ahora, vamos a añadir CHANNEL_LAYER a settings.py. Copia y pega el siguiente fragmento de código:

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

Las tachuelas

Ahora, vayamos a lo importante.

Cree modelos para la lead_manager aplicación. Aquí, añadiremos modelos para Lead y Agent. El modelo Lead representará a los clientes y posibles clientes. El modelo Agent representará a los vendedores de SalesFox que estarán en contacto con los clientes. Copie y pegue el siguiente fragmento de código en lead_manager/models.py:

from django.db import models
from django.contrib.auth.models import AbstractUser


#Users are staffs or partners that use the CRM
class User(AbstractUser):
    country = models.CharField(max_length=100, blank=True)
    address = models.CharField(max_length=200, blank=True)
    phone_number = models.CharField(max_length=15, blank=True)

  def __str__(self):
      return self.username


class Lead(models.Model):
    LEAD_SOURCES = (
        ('organic_search', 'Organic Search'),
        ('google_ad', 'Google Ad'),
        ('youtube', 'YouTube'),
        ('facebook', 'Facebook'),
        ('instagram', 'Instagram'),
        ('twitter', 'Twitter'),
    )

    MEDIA_CHOICES = (
        ('sms', 'SMS'),
        ('facebook', 'Facebook'),
        ('phone_call', 'Phone call')
    )

    first_name = models.CharField(max_length=25, blank=True)
    last_name = models.CharField(max_length=25, blank=True)
    age = models.IntegerField(default=0)

    facebook_id = models.CharField(max_length=100, blank=True)
    phone_number = models.CharField(max_length=15, blank=True)

    source = models.CharField(
        choices=LEAD_SOURCES, 
        max_length=50,
        blank=True,
        help_text="Where Lead found us",
        default=LEAD_SOURCES[3][0]
    )
    preferred_medium = models.CharField(
        choices=MEDIA_CHOICES, 
        max_length=50,
        default=MEDIA_CHOICES[1][0],
        help_text="Lead's preferred social media for communication"
    )
    active = models.BooleanField(default=False)

    profile_picture = models.ImageField(blank=True, null=True)

    date_created = models.DateTimeField(auto_now_add=True)
    date_updated = models.DateTimeField(auto_now=True)

    agent = models.ForeignKey("Agent", on_delete=models.SET_NULL, null=True, blank=True, related_name='leads')

    def __str__(self):
        return self.first_name

    @property
    def has_agent(self):
        return self.agent is not None


class Agent(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='agent')

    def __str__(self):
        return self.user.username

Creamos un modelo de Usuario para representar a cada usuario en SalesFox - Esto podría ser community managers, representantes de región, soporte al cliente, etc. Sin embargo, para mantener SalesFox lo más ligero posible, el único tipo de usuarios que tenemos son los agentes.

El modelo Lead representa a los clientes potenciales que contactan desde su cuenta de Facebook. El campo facebook_id representa el ID de la cuenta de Facebook de un cliente. Es el campo que necesitamos para que los agentes envíen un mensaje directo a los clientes en Facebook. El modelo Lead también tiene un campo preferred_medium. En él se indica el medio de comunicación preferido por el cliente. Nos centraremos únicamente en la comunicación a través de Facebook.

`AUTH_USER_MODEL = 'lead_manager.User'`

Ahora, vamos a crear un Message modelo en la aplicación de conversación. El modelo Message representa un único mensaje enviado desde/a SalesFox. Copia y Pega el siguiente fragmento de código en models.py de la conversation aplicación.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

LEAD_MODEL = models.Q(app_label='lead_manager', model='Lead')
AGENT_MODEL = models.Q(app_label='lead_manager', model='Agent')
communicating_parties = LEAD_MODEL | AGENT_MODEL

class Message(models.Model):
    body = models.TextField()

    sender_type = models.ForeignKey(
        ContentType, 
        limit_choices_to=communicating_parties,
        null=True, blank=True, on_delete=models.SET_NULL, related_name="sent_messages"
    ) 
    sender_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
    sender = GenericForeignKey(ct_field='sender_type', fk_field='sender_id')

    receiver_type = models.ForeignKey(
        ContentType, 
        limit_choices_to=communicating_parties,
        null=True, blank=True, on_delete=models.SET_NULL, related_name="received_messages"
    )
    receiver_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
    receiver = GenericForeignKey(ct_field='receiver_type', fk_field='receiver_id')

    date_created = models.DateTimeField(auto_now_add=True)
    message_key = models.CharField(null=True, blank=True, max_length=50)
    is_delivered = models.BooleanField(default=False)

    def __str__(self):
        return "Message (%s) from %s to %s" % (self.id, self.sender, self.receiver)

En nuestro Message tenemos dos relaciones genéricas para identificar al remitente y al destinatario del mensaje. El remitente y el destinatario pueden ser un lead o un agente. Esto significa que sólo los agentes o los clientes potenciales pueden enviar o recibir mensajes. Visite aquí para aprender más sobre las relaciones genéricas en Django.

Crear un método de propiedad messages para el modelo Lead en lead_manager/models.py. Este método devuelve todos los mensajes entrantes y salientes de un lead.

En lead_manager/models.py, pega las siguientes sentencias import.

En el modelo Lead cree el método de propiedad "mensajes" como en el siguiente fragmento:

from  django.contrib.contenttypes.models  import  ContentType
from  django.db.models  import  Value
from  itertools  import  chain

@property
def messages(self):
  from conversation.models import Message
  message_type = ContentType.objects.get_for_model(self)
  msgFromLead = Message.objects.filter(sender_id=self.id, sender_type=message_type).annotate(
      from_lead=Value(True, models.BooleanField())
  )

  msgToLead =  Message.objects.filter(receiver_id=self.id, receiver_type=message_type).annotate(
      from_lead=Value(False, models.BooleanField())
  )
  messages = sorted(
      chain(msgFromLead, msgToLead), 
      key=lambda instance: instance.date_created
  )

  return messages

Empecemos con las vistas.

En lead_manager, crearemos vistas para realizar operaciones CRUD en el modelo Lead. Ve a la carpeta de la app lead_manager, luego copia y pega el siguiente código en views.py para crear las vistas:

from django.shortcuts import render, redirect
from django.urls import reverse
from django.views.generic import TemplateView, ListView, UpdateView, CreateView
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from .models import Lead
from .forms import LeadForm	
    
class HomeView(TemplateView):
    template_name = 'index.html'


class LeadListView(ListView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_list.html'
    queryset = Lead.objects.all()
    context_object_name = 'leads'

    def dispatch(self, request, *args, **kwargs):
        if not (request.user.is_superuser and hasattr(request.user, 'agent')):
            return PermissionDenied
        return super().dispatch(request, *args, **kwargs)


class LeadCreateView(CreateView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_create.html'
    form_class = LeadForm

    def dispatch(self, request, *args, **kwargs):
        if not (request.user.is_superuser and hasattr(request.user, 'agent')):
            return PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('lead_manager:lead_list')


class LeadUpdateView(UpdateView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_update.html'
    queryset = Lead.objects.all()
    form_class = LeadForm

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(request.user, 'agent'):
            raise PermissionDenied

        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        messages.success(self.request, "{}'s info is successfully updated".format(self.get_object()))
        return reverse('lead_manager:lead_update', args=[self.get_object().id])


@login_required
def lead_delete(request, pk):
    if not request.user.is_superuser:
        return PermissionDenied

    lead = Lead.objects.only('id').get(id=pk)
    lead.delete()

    return redirect('lead_manager:lead_list')

En las vistas anteriores, sobrescribimos el método dispatch para gestionar los permisos de cada vista.

Cree forms.py dentro del directorio de la aplicación lead_manager. En forms.pydefine LeadForm:

from django import forms
from .models import Lead

class LeadForm(forms.ModelForm):
    class Meta:
        model = Lead
        fields = [
            'first_name', 
            'last_name', 
            'age',
            'facebook_id',
            'phone_number',
            'source', 
            'preferred_medium', 
            'agent'
        ]

Cree views.py dentro de una subcarpeta en lead_manager llamada agent. Y defina sus AgentLoginView y AgentDashboardView vistas.

from django.contrib.auth.views import LoginView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.views.generic.base import TemplateView

class AgentLoginView(LoginView):
    template_name = 'lead_manager/agent_login.html'


class AgentDashboardView(LoginRequiredMixin, TemplateView):
    template_name = 'lead_manager/agent_dashboard.html'

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(request.user, 'agent'):
            raise PermissionDenied

        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        assigned_leads = request.user.agent.leads.all()
        context = {
            'assigned_leads': assigned_leads,
        }

        return self.render_to_response(context)

Creemos lead_manager/urls.py y lead_manager/agent/urls.py.

Vaya al directorio lead_manager y cree un archivo urls.py archivo. Ahora, defina patrones de URL para las vistas de lead_manager.

from django.urls import path
from . import views

app_name = 'lead_manager'
urlpatterns = [
    path('', views.LeadListView.as_view(), name='lead_list'),
    path('create/', views.LeadCreateView.as_view(), name='lead_create'),
    path('<int:pk>/update/', views.LeadUpdateView.as_view(), name='lead_update'),
    path('<int:pk>/delete/', views.lead_delete, name='lead_delete'),
]
```
In your lead_manager directory, go to `agent` folder and create a file named `urls.py`. Define URL patterns for agent views as in the snippet below:

```
from django.contrib.auth.views import LogoutView
from django.urls import path
from .views import AgentLoginView, AgentDashboardView  

app_name = 'agent'
urlpatterns = [
    path('login/', AgentLoginView.as_view(), name='agent_login'),
    path('logout/', LogoutView.as_view(), name='agent_logout'),
    path('dashboard/', AgentDashboardView.as_view(), name='agent_dashboard'),
]

Desde los dos urls.py en la aplicación lead_manager, puedes confirmar que todas las vistas que hemos creado en la aplicación lead_manager tienen sus correspondientes configuraciones de URL.

Ahora, informemos a Django de la URL de inicio de sesión, la URL de redirección de inicio de sesión y la URL de redirección de cierre de sesión. Añade lo siguiente a settings.py

LOGIN_URL = 'agent:agent_login'
LOGIN_REDIRECT_URL = 'agent:agent_dashboard'
LOGOUT_REDIRECT_URL = 'home'

Ahora, pasemos a la aplicación de conversación.

Además de las vistas y la configuración de URL, también configurarás un consumidor web-socket en la app de conversación. Permitirá la comunicación entre los agentes de SalesFox y los leads en tiempo real.

Vamos a crear la lead_conversation_room para la sala de conversación. Ve a views.py en la carpeta de conversación y pega el siguiente fragmento de código

from  django.shortcuts  import  render
from  django.contrib.auth.decorators  import  login_required
from  django.http  import  HttpResponse, HttpResponseForbidden
from django.core.exceptions import PermissionDenied
from lead_manager.models import Lead	
    
@login_required
def lead_conversation_room(request, lead_id):
    if not hasattr(request.user, 'agent'):
        return PermissionDenied

    agent = request.user.agent
    try:
        lead = agent.leads.get(id=lead_id)
    except Lead.DoesNotExist:
        return HttpResponseForbidden()

    context = {"lead": lead}
    return render(request, "conversation/room.html", context)

La vista lead_conversation_room gestiona las solicitudes realizadas por los agentes para abrir una sala de conversación con un cliente.

Ahora, cree send_outbound la función. send_outbound La función es responsable de enviar mensajes desde SalesFox a los clientes en Facebook Messenger. Se necesita el mensaje que se enviará y el ID de Facebook de plomo como argumentos.

from django.conf  import  settings
import  requests
import  json
import  base64
from  requests.exceptions  import  ConnectionError

def send_outbound(message, lead_facebook_id):
    url = settings.VONAGE_MESSAGES_ENDPOINT

    auth_param = settings.VONAGE_API_KEY + ":" + settings.VONAGE_API_SECRET
    auth_code = base64.b64encode(auth_param.encode('utf-8'))

    payload = json.dumps({
    "from": {
        "type": "messenger",
        "id": settings.FACEBOOK_ID
    },
    "to": {
        "type": "messenger",
        "id": lead_facebook_id
    },
    "message": {
        "content": {
        "type": "text",
        "text": message
        }
    }
    })
    headers = {
    'Authorization': 'Basic %s' % auth_code.decode('utf-8'),
    'Accept': 'application/json',
    'Content-Type': 'application/json'
    }
    try:
        response = requests.request("POST", url, headers=headers, data=payload)
    except ConnectionError:
        return
    return response

Como queremos comunicación en tiempo real entre clientes potenciales y agentes en la sala de conversación, necesitamos crear un WebSocket en el lado del cliente y configurar un consumidor WebSocket en el backend.

En la carpeta de la aplicación de conversación, crea una carpeta consumers.py carpeta. En la carpeta consumers.pycrea una clase de consumidor WebSocket - . ConversationConsumer.

import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from django.contrib.contenttypes.models import ContentType

from .models import Message
from .views import send_outbound
from lead_manager.models import Lead, Agent

def create_conversation_group(convo_id):
    return "conversation_%s" % convo_id

class ConversationConsumer(WebsocketConsumer):
    def connect(self):
        self.lead_id = self.scope['url_route']['kwargs']['lead_id']
        self.conversation = create_conversation_group(self.lead_id)
        self.agent = getattr(self.scope['user'], 'agent', None)
        
        try:
            self.lead = Lead.objects.get(id=self.lead_id)
        except Lead.DoesNotExist:
            self.lead = None

        # join conversation
        async_to_sync(self.channel_layer.group_add)(
            self.conversation,
            self.channel_name
        )

        self.accept()
    
    def disconnect(self, exit_code):
        # leave conversation
        async_to_sync(self.channel_layer.group_discard)(
            self.conversation,
            self.channel_name
        )

    def save_message(self, message_data):
        if self.agent:
            sender_type = ContentType.objects.get_for_model(self.agent)
            message_data['sender_type'] = sender_type
            message_data['sender_id'] = self.agent.id
        
        if self.lead:
            receiver_type = ContentType.objects.get_for_model(self.lead)
            message_data['receiver_type'] = receiver_type
            message_data['receiver_id'] = self.lead.id

        message = Message.objects.create(**message_data)
        return message

    def receive(self, text_data):
        data = json.loads(text_data)
        message = data['message']
        saved_message = self.save_message({'body': message})

        if self.lead:
            # send message to Lead on social media (Facebook)
            response = send_outbound(message, self.lead.facebook_id)
            if response and response.ok:
                response_data = response.json()
                saved_message.is_delivered = True
                saved_message.message_key = response_data["message_uuid"]
                saved_message.save()

        # send message to everyone connected to the conversation
        async_to_sync(self.channel_layer.group_send)(
            self.conversation,
            {
                'type': 'send_to_conversation',
                'message': message,
                'from_agent': True
            }
        )
    
    def send_to_conversation(self, event):
        # send message to Websocket
        self.send(
            json.dumps(
                {
                    "message": event['message'],
                    "from_agent": event['from_agent'],
                }
            )
        )

Para explicar los métodos - Por cada agente que abre la página de conversación, hay una llamada a ConversationConsumer. El resultado es un nuevo canal para el agente.

  • connect()es llamado cuando se recibe una conexión WebSocket. Aquí, añadimos el canal del agente a una conversación y luego aceptamos la conexión.

  • disconnect(): Aquí, eliminamos el canal del agente de la conversación.

  • receive(): Aquí, recibimos un nuevo mensaje del cliente. Después llamamos a save_message que guarda el mensaje en nuestra base de datos. A continuación, enviamos el mensaje como un mensaje directo de Facebook para el plomo llamando a send_outbound. A continuación, el mensaje se envía a la sala de conversación. Al final del método receive el mensaje se enviará a todos los agentes de la sala de conversación.

  • save_message(): Aquí guardamos el mensaje del agente en la base de datos. Esto se llama en receive

  • send_to_conversation(): Se utiliza para transmitir el mensaje del agente a la sala de conversación, de forma que todos los agentes de la sala puedan ver el mensaje.

Ahora, vamos a configurar el enrutamiento para nuestro . ConversationConsumer.

Crea routing.py en el directorio de aplicaciones de conversación y pega lo siguiente:

from  django.urls  import  re_path
from .consumers  import  ConversationConsumer

websocket_urlpatterns = [
re_path(r'ws/conversation/(?P<lead_id>\d+)/$', ConversationConsumer),
]

Cree un archivo routing.py en el directorio del proyecto. Este archivo contiene la configuración de enrutamiento global para el proyecto.

from  channels.routing  import  ProtocolTypeRouter, URLRouter
from  channels.auth  import  AuthMiddlewareStack
from  conversation  import  routing

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(URLRouter(
        routing.websocket_urlpatterns
    )),
})

Ahora, la referencia application en settings.py como aplicación ASGI que se ejecutará cuando Sales-Fox se sirva a través de la interfaz de pasarela de servidor asíncrono:

ASGI_APPLICATION = 'sales_fox.routing.application'

Vamos a crear una inbound vista. La vista inbound recibe el mensaje de un cliente de Vonage, guarda el mensaje y lo envía a los agentes de la sala de conversación.

from  channels.layers  import  get_channel_layer
from  asgiref.sync  import  async_to_sync
from  django.views.decorators.http  import  require_POST
from  django.views.decorators.csrf  import  csrf_exempt
from  django.contrib.contenttypes.models  import  ContentType
from  lead_manager.models  import  Lead
from .models  import  Message


@require_POST
@csrf_exempt
def inbound(request):
    from .consumers import create_conversation_group
    body = json.loads(request.body)
    channel_layer = get_channel_layer()

    message = body["message"]["content"].get("text")
    lead_facebook_id = body["from"]["id"]
    lead, _ = Lead.objects.get_or_create(facebook_id=lead_facebook_id)
    if message:
        sender_type = ContentType.objects.get_for_model(lead)
        sender_id = lead.id

        message_data = dict(body=message, sender_type=sender_type, sender_id=sender_id)
        agent = lead.agent
        if agent:
            receiver_type = ContentType.objects.get_for_model(agent)
            receiver_id = agent.id

            message_data["receiver_type"] = receiver_type
            message_data["receiver_id"] = receiver_id

        message_obj = Message.objects.create(**message_data)

        conversation_group = create_conversation_group(lead.id)
        try:
            async_to_sync(channel_layer.group_send)(
                conversation_group,
                {
                    "type": "send_to_conversation",
                    "message": message,
                    "from_agent": False, 
                } 
            )

            message_obj.is_delivered = True
            message_obj.save()
        except Exception as e:
            print("Something went wrong")
            print(e)

    with open('inbound.txt', 'w') as inbound_file:
        json.dump(body, inbound_file, sort_keys=True, indent=2)
    return HttpResponse(status=204)

Vonage envía actualizaciones de estado de mensajes a través del punto final de estado.

Como no vamos a utilizar la información de estado en este tutorial, vamos a crear una vista de estado simple para escribir el cuerpo de la petición en un archivo status.txt archivo.

En el archivo views.py de la aplicación de conversación, copia lo siguiente para crear la vista de estado.

@require_POST
@csrf_exempt
def  status(request):
	body = json.loads(request.body)
	with  open('status.txt', 'w') as  status_file:
		json.dump(body, status_file)
	return  HttpResponse(status=204)

Vamos a crear configuraciones de URL para la app de conversación. Ve al directorio de la aplicación de conversación y crea el archivo urls.py el archivo A continuación, copia y pega el siguiente fragmento de código:

from django.urls import path
from .views import inbound, status, lead_conversation_room

app_name = 'conversation'
urlpatterns = [
    path('inbound/', inbound, name='conversation-inbound'),
    path('status/', status, name='conversation-status'),
    path('lead/<int:lead_id>/', lead_conversation_room, name="lead-conversation-room"),
]

Vaya al directorio del proyecto y busque el archivo urls.py. Este archivo está en el mismo directorio que settings.py. Ahora, copia y pega el siguiente código:

from django.urls import path
from .views import inbound, status, lead_conversation_room

app_name = 'conversation'
urlpatterns = [
    path('inbound/', inbound, name='conversation-inbound'),
    path('status/', status, name='conversation-status'),
    path('lead/<int:lead_id>/', lead_conversation_room, name="lead-conversation-room"),
]

Ahora que hemos terminado con el backend de nuestro proyecto. Vamos a crear los archivos frontend.

Vaya a su carpeta estática en el directorio general y cree una carpeta llamada css. En css cree dos archivos style.css y chat.css.

En styles.csscopie y pegue los siguientes estilos

.container {
    margin: 30px;
}

.link-group {
    display: inline-flex; 
    column-gap: 20px;
}

a {
    text-decoration: none;
}

.list {
    margin-bottom: 20px;
}

En chat.css, copia y pega los siguientes estilos:

.container {
    max-width: 500 !important;
    margin: auto;
    margin-top: 4%;
    letter-spacing: 0.5px;
}

.msg-header {
    border: 1px solid #ccc;
    width: 100%;
    height: 10%;
    border-bottom: none;
    display: inline-block;
    background-color: #007bff;
}

.active {
    width: 120px;
    float: left;
    margin-top: 10px;
}

.active h4 {
    font-size: 20px;
    margin-left: 10px;
    color: #fff;
}

.msg-inbox {
    border: 1px solid #ccc;
    overflow: hidden;
    padding-bottom: 20px;
}

.chats {
    padding: 30px 15px 0 25px;

}

.msg-page {
    height: 400px;
    overflow-y: auto;
}

.received-msg {
    display: inline-block;
    padding: 0 0 0 10px;
    vertical-align: top;
    width: 53%;
}

.received-msg p {
    background: #efefef none repeat scroll;
    border-radius: 10px;
    color: #646464;
    font-size: 14px;
    margin: 0;
    padding: 5px 10px 5px 12px;
    width: 100%;
}

.time {
    color: #777;
    display: block;
    font-size: 12px;
    margin: 8px 0 0;
}
.outgoing-msg {
    float: left;
    width: 46%;
    margin-left: 45%;
}

.outgoing-msg p {
    background: #007bff none repeat scroll 0 0;
    color: #fff;
    border-radius: 10px;
    font-size: 14px;
    margin: 0;
    padding: 5px 10px 5px 12px;
    width: 100%;
}

.msg-bottom {
    position: relative;
    width: 100%;
    height: 20%;
    background: #007bff;
    display: inline-block;
}

.input-group {
    float: right;
    margin: 10px 20px 10px 0;
    outline: none !important;
    border-radius: 20px;
    width: 61% !important;
    background-color: #fff;
}

.form-control {
    border: none !important;
    border-radius:  20px !important;
}

.input-group-text {
    background: transparent !important;
    border: none !important;
    color: #007bff;
    cursor: pointer;
}

.input-group-append {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.input-group .fa {
    color: #007bff;
    float: right;
}

.bottom-icons {
    float: left;
    margin-top: 17ox;
    width: 30px !important;
    margin-left: 22px;
}

.bottom-icons .fa {
    color: #007bff;
    padding: 5px;
}

.form-control:focus {
    border-color: none !important;
    box-shadow: none !important;
}

Usaremos el archivo chat.css para la conversación room.html mientras que usaremos styles.css para otras páginas. Ahora, en tu directorio general, crea una carpeta llamada templates y crea dos archivos HTML - base.html y index.html. Extenderás base.html en todos los demás archivos HTML excepto en la conversación room.html.

En base.html, copia y pega lo siguiente

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{% static 'css/style.css' %}">

    <title>{% block title %} Sales Fox {% endblock title %}</title>
</head>
<body>
    <div class="container">
    {% block content %}
    {% endblock content %}
    </div>
</body>
{% block script %}
{% endblock script %}
</html>

En index.html (página de inicio), copie y pegue lo siguiente

{% extends 'base.html' %}
{% load static %}
{% block content %}

Ahora, vaya al directorio de la aplicación lead_manager. Cree una carpeta templates y en templatescree otra carpeta lead_manager. En lead_manager/templates/lead_manager, cree cinco archivos html - lead_list.html, lead_create.html, lead_update.html, agent_login.html, agent_dashboard.html.

lead_list.html,

{% extends 'base.html' %}
  {% load static %}

  {% block content %}
      <a href="{% url 'agent:agent_dashboard' %}">Go to dashboard</a>
      <h4>List of leads</h4>
      <ul>
          {% for lead in leads %}
          <li class="list">
              <div class="link-group">
                  <div style="width: 100px;">
                      {{lead.first_name}} ({{lead.id}})
                  </div>
                  <a href="{% url 'lead_manager:lead_update' lead.id %}">Update</a>
                  <a href="{% url 'lead_manager:lead_delete' lead.id %}"> Delete</a>
                  {% if not lead.has_agent %} | <span style="color: red;">Not Assigned</span> {% endif %}
              </div>
          </li>
          {% empty %}
          <p>Lead list is empty</p>
          {% endfor %}
      </ul> 

  <div class="create_lead_link">
      <a href="{% url 'lead_manager:lead_create' %}">Create new lead</a>
  </div>
{% endblock content %}

lead_create.html,

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <a href="{% url 'lead_manager:lead_list'  %}">Go to lead list</a>
    <h1>Lead Creation Form</h1>

    <form action="." method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Send">
    </form>
{% endblock content %}

lead_update.html,

{% extends 'base.html' %}
{% load static %}	

{% block content %}
    <a href="{% url 'agent:agent_dashboard' %}">Go to dashboard</a>

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li{% if message.tags %}>{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <form action="." method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Send">
    </form>
{% endblock content %}
	

agent_login.html

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <h1>Login to your dashboard</h1>

<form action="." method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock content %}

agent_dashboard.html

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <h4>List of leads assigned to you</h4>

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li{% if="" message.tags="" %}="" class="message-{{ message.tags }}" {%="" endif="">{{ message }}
        {% endfor %}
    </li{%></ul>
    {% endif %}

    <div>
        <ul>
            {% for lead in assigned_leads %}
            <li class="list">
                <div class="link-group">
                    <div style="width: 100px;">
                        {{lead.first_name}} ({{lead.id}})
                    </div>
                    <a href="{% url 'lead_manager:lead_update' lead.id %}">Update</a>
                    <a href="{% url 'conversation:lead-conversation-room' lead.id %}">Go to conversation room</a>
                    </div>
            </li>
            {% empty %}
            <p>No assigned lead</p>
            {% endfor %}
        </ul>
    </div>

    <div class="link-group">
        {% if request.user.is_superuser %}    
        <a href="{% url 'lead_manager:lead_list' %}">View lead list</a>
        <a href="{% url 'lead_manager:lead_create' %}">Create new lead</a>    
        {% endif %}
        <a href="{% url 'agent:agent_logout' %}">Logout</a>
    </div>
{% endblock content %}

En el directorio de la aplicación de conversación, cree una carpeta templates y en la carpeta templates cree una subcarpeta conversation.

Dentro de la carpeta "conversation/templates/conversation", crea un archivo room.html archivo. Copie y pegue lo siguiente:

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" href="{% static 'css/chat.css' %}">
    <title>Conversation with {{lead}}</title>
</head>
<body>
    <div class="container">
        <a href="{% url 'agent:agent_dashboard' %}">Go to Dashboard</a>
        <div class="msg-header">
            <div class="active">
                <h4>{{lead.first_name}} {{lead.last_name}}</h4>
            </div>
        </div>

        <div class="conversation">
            <div class="msg-inbox">
                <div class="chats">
                    <div class="msg-page" id="msgPage">
                        {% for message in lead.messages %}
                        {% if message.from_lead %}
                        
                        <div class="received-msg">
                            <div class="received-msg-inbox">
                                <p>{{message.body}} {{message.from_lead}}</p>
                                <span class="time">{{message.date_created}}</span>
                            </div>
                        </div>
                        
                        {% else %}
                        <div class="outgoing-msg">
                            <p>{{message.body}}</p>
                            <span class="time">{{message.date_created}}</span>
                        </div>
                        {% endif %}
                        {% endfor %}
                    </div>
                </div>
            </div>

            <div class="msg-bottom">
                <div class="input-group">
                    <textarea name="message" id="msgWriter" rows="3" class="form-control"></textarea>
                    <div class="input-group-append">
                        <span class="input-group-text" id="send">Send</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        const selectElement = (e) => document.querySelector(e);

        let messagePage = selectElement("#msgPage");
        let msgWriter = selectElement("#msgWriter");
        const msgType = {agent: 'outgoing', lead: 'received'}
        let socket = null

        const keepScrollToEnd = () => {
            messagePage.scrollTop = messagePage.scrollHeight
        }

        const getMessageBox = (text, date, type=msgType.agent) => {
            const parentDiv = document.createElement('div')
            parentDiv.classList.add(`${type}-chats`)

            const childDiv = document.createElement('div')
            childDiv.classList.add(`${type}-msg`)

            const msgParagraph = document.createElement('p')
            msgParagraph.textContent = text

            const dateSpan = document.createElement('span')
            dateSpan.classList.add('time')
            dateSpan.textContent = date

            childDiv.append(msgParagraph, dateSpan)
            parentDiv.append(childDiv)

            return parentDiv
        }

        // Displays new message in messagePage
        const showNewMessage = (val) => {
            let msgElement
            if (val.from_agent){
                msgElement = getMessageBox(text=val.message, date=val.date, type=msgType.agent)
            } else {
                msgElement = getMessageBox(text=val.message, date=val.date, type=msgType.lead)
            }
            messagePage.append(msgElement);
            keepScrollToEnd()
        }

         
        function sendMessage(event) {
            if (!msgWriter.value) return false;
            if (!socket) {
                alert("No socket connection. Reload browser");
                return false
            }

            socket.send(JSON.stringify({"message": msgWriter.value}));
            msgWriter.value = "";
            event.preventDefault();
            return false
        }

        if (!window["WebSocket"]) {
            alert("Your browser does not support web sockets. Change browser");
        } else {
            var conversationURL = "ws://" + window.location.host + "/ws/conversation/" + "{{ lead.id }}/"
            socket = new WebSocket(conversationURL);
            socket.onclose = function(){
                alert("Web socket connection has been closed");
            }

            // calls showNewMessage if socket receives message
            socket.onmessage = function(msg) {
                showNewMessage(JSON.parse(msg.data));
            }
        }
        selectElement("#send").addEventListener("click", sendMessage, false);
    </script>
</body>
</html

Antes de la etiqueta de cierre para el elemento body en room.html, tenemos un script que maneja la operación WebSocket y la renderización del mensaje en la sala de conversación.

Ponga en marcha SalesFox

Ya hemos completado el desarrollo de SalesFox.

Siga estos pasos para que SalesFox funcione localmente.

  1. Ejecute redis-server para iniciar Redis. Puede detener el servidor Redis de forma segura ejecutando redis-cli shutdown

  2. Crea un túnel HTTP con Ngrok que reenvíe las peticiones al puerto desde el que estás ejecutando SalesFox. Esto le proporciona una URL pública disponible para su SalesFox localhost:port. Aprende más sobre esto aquí.

  3. Vaya al archivo .env en su directorio general. Define una nueva variable env llamada HOST con la URL de tu túnel Ngrok.

    HOST=4339-197-210-53-35.ngrok.io
  4. Añada HOST del archivo .env a ALLOWED_HOST en settings.py. ALLOWED_HOST definición en settings.py debería tener este aspecto:

    ALLOWED_HOSTS = [os.getenv('HOST'), "localhost", "127.0.0.1"].
  5. Recuerda que completamos URL falsas como URL de entrada y de estado en nuestra página de la aplicación de Vonage. Ahora, reemplazaremos estas URL con los valores correctos. Como mi host de túnel es http://4339-197-210-53-35.ngrok.iomi URL de entrada será http://4339-197-210-53-35.ngrok.io/conversation/inbound y mi URL de estado será http://4339-197-210-53-35.ngrok.io/conversation/status.

    Ve a la página de tu aplicación de Vonage y actualiza los campos Inbound URL y Status URL y

  6. Ahora, ve a tu terminal (asegúrate de que estás en el directorio general). Luego, ejecute python manage.py runserver para servir a SalesFox en el puerto 8000.

    python manage.py runserver 9000.

Conclusión

Si has llegado hasta aquí, gracias por construir este proyecto conmigo. En el curso de la construcción de SalesFox, nos hemos ceñido al mínimo posible de características y diseño. Sin embargo, Usted puede hacer mucho más mediante la creación de más características sobre SalesFox.

Puedes agregar más opciones de preferred_medium para clientes potenciales. Vonage ofrece diversas API de comunicación, algunas de las cuales puedes desarrollar para que SalesFox las admita. Vale la pena revisarlas aquí.

¡Salud!

Compartir:

https://a.storyblok.com/f/270183/400x400/5625a429b4/tolulope-olanrewaju.png
Tolulope OlanrewajuAutor invitado

Tolulope es un ingeniero de software afincado en Nigeria. Le encanta crear soluciones multiplataforma y herramientas de comunicación para personas y empresas. Cuando no está trabajando, le encanta leer libros sobre diseño de productos, humanidad y emprendimiento.