https://a.storyblok.com/f/270183/24305/81f32c5594/social-crm_django.png

Criação de um CRM social com o Django e a Messages API do Vonage

Publicado em January 4, 2022

Tempo de leitura: 11 minutos

Neste artigo, você aprenderá a desenvolver o recurso principal de um CRM social usando o Django e a Messages API do Vonage. Nosso CRM social ajudará os agentes de vendas e a equipe de atendimento ao cliente a se comunicarem com clientes em potencial diretamente no Facebook, em tempo real. Vamos chamá-lo de Sales Fox.

Pré-requisitos

  1. Crie uma aplicação de mensagens a partir do seu painel do Vonage. Siga as etapas descritas aqui.

  2. Autorize a Vonage a acessar sua página comercial do Facebook e vincule seu aplicativo à sua página do Facebook. Siga as etapas descritas aqui.

  3. Instale o Redis — Se você estiver usando Linux ou Mac, siga as instruções aqui. Se você estiver usando o Windows, siga as instruções aqui.

  4. Instale o Ngrok. Acesse página de download do Ngrok e siga as instruções para configurar o Ngrok no seu computador.

Agora que você já cumpriu todos os pré-requisitos, é preciso configurar seu ambiente de desenvolvimento para o tutorial.

Configuração do projeto

  1. Crie e ative seu ambiente virtual

    Crie uma pasta para o seu projeto e mude o diretório de trabalho para a pasta que você acabou de criar. Em seguida, execute os comandos a seguir para criar e ativar um ambiente virtual para o seu projeto.

    python3 -m venv sales-env

    source sales-env/bin/activate

  2. Instalar os pacotes necessários

    Para instalar todos os pacotes necessários de uma só vez, crie um requirements.txt arquivo no diretório criado na etapa 1. Copie e cole o trecho de código abaixo no seu requirements.txt arquivo.

    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

    Agora, instale todos os pacotes requirements.txt executando o comando abaixo no seu terminal.

    pip install -r requirements.txt

  3. Crie seu projeto Django

  • Execute django-admin startproject sales_fox o seguinte comando para criar o projeto Django chamado “sales_fox”.

  • Vamos criar dois aplicativos no sales_fox: o lead_manager aplicativo para gerenciar leads e o conversation aplicativo para que os agentes de vendas se comuniquem com clientes em potencial (conhecidos como leads). Agora, vamos criar nossos dois aplicativos executando estes comandos.

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

Observe que, neste tutorial,

  • Vou usar os termos “leads” e “clientes” de forma intercambiável. Os leads são clientes em potencial; portanto, não há problema em considerá-los clientes quando for conveniente.

  • Vou usar o termo Project Directory para me referir ao diretório onde você tem settings.py. Esse diretório foi criado quando você executou django-admin startproject sales_fox.

  • Vou usar o termo Overall Directory para me referir ao diretório que você criou no início do tutorial. Ele contém a pasta do seu ambiente virtual, os diretórios dos aplicativos e o diretório do seu projeto

  1. Vamos preparar o SalesFox para usar o Vonage.

  • Crie um .env arquivo no seu diretório principal. Defina FACEBOOK_ID, VONAGE_API_KEY, e VONAGE_API_SECRET. Seu arquivo .env deve ficar assim:

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

    Você pode encontrar sua chave API e seu segredo API da Vonage na sua página de configurações da Vonage. E seu ID do Facebook pode ser encontrado na Link social channels aba na página do seu aplicativo.

    No diretório do seu projeto, vá para settings.py, carregue as variáveis do seu arquivo .env usando python-dotenv instalado a partir de requirements.txt. Adicione o seguinte trecho em settings.py para carregar o arquivo .env:

    from  dotenv  import  load_dotenv	
    import  os
    load_dotenv()

load_dotenv carrega todas as variáveis do nosso arquivo .env como variáveis de ambiente. Agora, defina FACEBOOK_ID, VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_MESSAGES_ENDPOINT no seu settings.py arquivo. Basta copiar e colar o trecho abaixo.

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 arquivos estáticos

Em settings.py, encontre STATIC_URL variável e adicione a STATICFILES_DIRS e STATIC_FILES abaixo STATIC_URL, você deve ter algo assim:

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

Vá até o diretório raiz e crie uma pasta chamada static. É aqui que você armazenará todos os seus arquivos estáticos. Observe que você só deve fazer isso em um ambiente de desenvolvimento. Em um ambiente de produção, você deve configurar um armazenamento externo, como um bucket do AWS S3, para servir seus arquivos estáticos.

  1. Atualizar os aplicativos instalados e definir a camada de canal

Precisamos adicionar channels e os aplicativos que criamos (lead_manager e conversation) INSTALLED_APPS no arquivo settings.py. Seu arquivo INSTALLED_APPS no settings.py deve ficar assim:

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

Os canais do Django nos ajudam a incluir suporte a WebSocket no SalesFox. Uma camada de canais introduz o uso de canais e grupos no SalesFox. Ela nos ajuda a incorporar funcionalidades distribuídas em nosso aplicativo. Você pode ler mais sobre camadas de canais aqui. Para este projeto, vou usar o Redis como nossa camada de canais. Já instalamos channels-redis a partir do requirements.txt. Agora, vamos adicionar CHANNEL_LAYER ao settings.py. Copie e cole o trecho de código abaixo:

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

O que realmente importa

Agora, vamos ao que interessa.

Crie modelos para o lead_manager aplicativo. Aqui, vamos adicionar modelos para Lead e Agente. O modelo Lead representará clientes e clientes em potencial. O modelo Agent representará os vendedores da SalesFox que estarão em contato com os clientes. Copie e cole o seguinte trecho de código em 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

Criamos um modelo “Usuário” para representar todos os usuários do SalesFox — que podem ser gerentes de comunidade, representantes regionais, equipe de atendimento ao cliente etc. No entanto, para manter o SalesFox o mais enxuto possível, o único tipo de usuário que temos são os agentes.

O Lead modelo representa clientes em potencial que entram em contato por meio de suas contas do Facebook. O campo facebook_id representa o ID da conta do Facebook de um cliente. É o campo de que precisamos para que os agentes enviem uma mensagem direta aos clientes no Facebook. O Lead modelo também possui um campo `preferred_medium`. Ele contém o meio de comunicação preferido do cliente. Vamos nos concentrar apenas na comunicação pelo Facebook.

`AUTH_USER_MODEL = 'lead_manager.User'`

Agora, vamos criar um Message modelo no aplicativo de conversação. O Message modelo representa uma única mensagem enviada de/para o SalesFox. Copie e cole o seguinte trecho de código em models.py da conversation aplicativo.

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)

Em nosso Message modelo, temos duas relações genéricas para identificar o remetente e o destinatário da mensagem. O remetente e o destinatário podem ser um lead ou um agente. Isso significa que apenas agentes ou leads podem enviar ou receber mensagens. Acesse aqui para saber mais sobre relações genéricas no Django.

Crie um método de propriedade messages para o Lead modelo em lead_manager/models.py. Esse método retorna todas as mensagens recebidas e enviadas de um lead.

No arquivo lead_manager/models.py, cole as seguintes instruções de importação.

No Lead modelo, crie o método de propriedade “messages”, conforme mostrado no trecho abaixo:

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

Vamos começar pelas vistas.

No lead_manager, vamos criar visualizações para realizar operações CRUD no modelo Lead. Acesse a pasta do aplicativo lead_manager e, em seguida, copie e cole o código a seguir no arquivo views.py para criar as visualizações:

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')

Nas visualizações acima, sobrescrevemos o método `dispatch` para lidar com as permissões de cada visualização.

Crie forms.py dentro do diretório do aplicativo lead_manager. Em forms.py, defina 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'
        ]

Crie views.py um arquivo dentro de uma subpasta no lead_manager chamada agent. E defina seu AgentLoginView e AgentDashboardView visualizações.

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)

Vamos criar lead_manager/urls.py e lead_manager/agent/urls.py.

Vá até o diretório lead_manager e crie um urls.py arquivo. Agora, defina padrões de URL para as visualizações do 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'),
]

A partir das duas urls.py no aplicativo lead_manager, é possível verificar que todas as visualizações que criamos no aplicativo lead_manager possuem configurações de URL correspondentes.

Agora, vamos informar ao Django a URL de login, a URL de redirecionamento de login e a URL de redirecionamento de logout. Adicione o seguinte ao arquivo settings.py

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

Agora, vamos passar para o aplicativo de conversas.

Além das visualizações e da configuração de URLs, você também configurará um consumidor de WebSocket no aplicativo de conversação. Isso permitirá a comunicação em tempo real entre os agentes do SalesFox e os leads.

Vamos criar a lead_conversation_room visualização para a sala de conversação. Vá para views.py na pasta “conversation” e cole o trecho de código abaixo

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)

O lead_conversation_room vista lida com as solicitações feitas pelos agentes para abrir uma sala de conversa com um cliente.

Agora, crie send_outbound função. send_outbound A função é responsável por enviar mensagens do SalesFox para os clientes no Facebook Messenger. Ela recebe como argumentos a mensagem a ser enviada e o ID do lead no Facebook.

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 uma comunicação em tempo real entre os clientes potenciais e os agentes na sala de conversação, precisamos criar um WebSocket no lado do cliente e configurar um consumidor de WebSocket no back-end.

Bem na pasta do aplicativo de conversas, crie uma consumers.py pasta. Na consumers.py, crie uma classe de consumidor de 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 os métodos — Para cada agente que abre a página de conversação, é feita uma chamada para ConversationConsumer. Isso resulta na criação de um novo canal para o agente.

  • connect(): é chamado quando uma conexão WebSocket é recebida. Aqui, adicionamos o canal do agente a uma conversa e, em seguida, aceitamos a conexão.

  • disconnect(): Aqui, removemos o canal do agente da conversa.

  • receive(): Aqui, recebemos uma nova mensagem do cliente. Em seguida, chamamos save_message que salva a mensagem em nosso banco de dados. Em seguida, enviamos a mensagem como uma mensagem direta do Facebook para o cliente em potencial, chamando send_outbound. A mensagem é então reenviada para a sala de conversa. Ao final do receive método, a mensagem será enviada a todos os agentes na sala de conversa.

  • save_message(): Aqui, salvamos a mensagem do agente no banco de dados. Isso é chamado em receive

  • send_to_conversation(): Usamos isso para divulgar a mensagem do agente na sala de conversação, de modo que todos os agentes presentes na sala possam ver a mensagem.

Agora, vamos configurar o roteamento para o nosso ConversationConsumer.

Crie routing.py no diretório do aplicativo de conversação e cole o seguinte:

from  django.urls  import  re_path
from .consumers  import  ConversationConsumer

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

Crie um routing.py arquivo no diretório do seu projeto. Esse arquivo contém a configuração global de roteamento do projeto.

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

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

Agora, consulte application no settings.py como aplicativo ASGI a ser executado quando o Sales-Fox for servido por meio da interface de gateway de servidor assíncrona:

ASGI_APPLICATION = 'sales_fox.routing.application'

Vamos criar uma inbound visualização. A inbound view recebe uma mensagem de um cliente da Vonage, salva a mensagem e a envia aos agentes na sala de conversação.

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)

A Vonage envia atualizações sobre o status das mensagens por meio do endpoint de status.

Como não vamos usar as informações de status neste tutorial, vamos criar uma visualização de status simples para gravar o corpo da solicitação em um status.txt arquivo.

No views.py arquivo do aplicativo de conversas, copie o seguinte para criar a visualização de status.

@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 criar configurações de URL para o aplicativo de conversação. Acesse o diretório do aplicativo de conversação e crie urls.py arquivo. Em seguida, copie e cole o trecho de código abaixo:

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"),
]

Vá até o diretório do projeto e localize o arquivo urls.py. Esse arquivo está no mesmo diretório que o settings.py. Agora, copie e cole o código a seguir:

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"),
]

Agora que já terminamos a parte de back-end do nosso projeto, vamos criar os arquivos de front-end.

Vá até a pasta “static” no diretório principal e crie uma pasta chamada css. Nessa css pasta, crie dois arquivos style.css e chat.css.

Em styles.css, copie e cole os seguintes estilos

.container {
    margin: 30px;
}

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

a {
    text-decoration: none;
}

.list {
    margin-bottom: 20px;
}

No arquivo chat.css, copie e cole os seguintes 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 o arquivo chat.css para a conversa room.html , enquanto usaremos o styles.css para as outras páginas. Agora, no diretório raiz, crie uma pasta chamada templates e crie dois arquivos HTML — base.html e index.html. Você irá estender base.html em todos os outros arquivos HTML, exceto no conversation room.html.

No arquivo base.html, copie e cole o seguinte

{% 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>

No arquivo index.html (página inicial), copie e cole o seguinte

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

Agora, vá até o diretório do aplicativo lead_manager. Crie uma pasta templates e, dentro dela templates, crie outra pasta lead_manager. Em lead_manager/templates/lead_manager, crie cinco arquivos 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 %}

No diretório do aplicativo de conversas, crie uma pasta templates e, nessa templates pasta, crie uma subpasta conversation.

Dentro da pasta “conversation/templates/conversation”, crie um room.html arquivo. Copie e cole o seguinte:

{% 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 da tag de fechamento do elemento `body` no arquivo room.html, temos um script que lida com a operação do WebSocket e a exibição de mensagens na sala de conversação.

Coloque o SalesFox em funcionamento

Já concluímos o desenvolvimento do SalesFox.

Siga estas etapas para executar o SalesFox localmente.

  1. Execute redis-server para iniciar o Redis. Você pode parar o servidor Redis com segurança executando redis-cli shutdown

  2. Crie um túnel HTTP com o Ngrok que encaminhe as solicitações para a porta na qual você está executando o SalesFox. Isso fornece a você uma URL pública para o seu SalesFox localhost:port. Saiba mais sobre isso aqui.

  3. Acesse o arquivo .env no diretório raiz. Defina uma nova variável de ambiente chamada HOST com o valor da URL do seu túnel do Ngrok.

    HOST=4339-197-210-53-35.ngrok.io
  4. Adicionar HOST do arquivo .env para ALLOWED_HOST em settings.py. ALLOWED_HOST definição em settings.py deve ficar assim:

    ALLOWED_HOSTS = [os.getenv('HOST'), "localhost", "127.0.0.1"]
  5. Lembre-se de que inserimos URLs fictícias como URLs de entrada e de status na página do nosso aplicativo da Vonage. Agora, vamos substituir essas URLs pelos valores corretos. Como o host do meu túnel é http://4339-197-210-53-35.ngrok.io, minha URL de entrada será http://4339-197-210-53-35.ngrok.io/conversation/inbound e minha URL de status será http://4339-197-210-53-35.ngrok.io/conversation/status.

    Acesse a página do seu aplicativo da Vonage e atualize o Inbound URL e Status URL campos.

  6. Agora, acesse seu terminal (certifique-se de estar no diretório raiz). Em seguida, execute python manage.py runserver para hospedar o SalesFox na porta 8000.

    python manage.py runserver 9000.

Conclusão

Se você chegou até aqui, obrigado por construir este projeto comigo. Ao desenvolver o SalesFox, nos limitamos ao mínimo possível de funcionalidades e design. No entanto, você pode fazer muito mais criando novas funcionalidades com base no SalesFox.

Você pode adicionar mais opções de “preferred_medium” para os leads. A Vonage oferece diversas APIs de comunicação, e algumas delas podem ser integradas ao SalesFox. Vale a pena conferi-las aqui.

Saúde!

Compartilhar:

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

Tolulope é um engenheiro de software que mora na Nigéria. Ele adora desenvolver soluções multiplataforma e ferramentas de comunicação para pessoas e empresas. Quando não está trabalhando, adora ler livros sobre design de produtos, humanidade e empreendedorismo.