
Compartilhar:
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.
Criação de um CRM social com o Django e a Messages API do Vonage
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
Crie uma aplicação de mensagens a partir do seu painel do Vonage. Siga as etapas descritas aqui.
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.
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.
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
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-envsource sales-env/bin/activateInstalar os pacotes necessários Para instalar todos os pacotes necessários de uma só vez, crie um
requirements.txtarquivo no diretório criado na etapa 1. Copie e cole o trecho de código abaixo no seurequirements.txtarquivo.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.0CopiarAgora, instale todos os pacotes
requirements.txtexecutando o comando abaixo no seu terminal.pip install -r requirements.txtCrie seu projeto Django
Execute
django-admin startproject sales_foxo seguinte comando para criar o projeto Django chamado “sales_fox”.Vamos criar dois aplicativos no sales_fox: o
lead_manageraplicativo para gerenciar leads e oconversationaplicativo 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 conversationCopiar
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 Directorypara me referir ao diretório onde você temsettings.py. Esse diretório foi criado quando você executoudjango-admin startproject sales_fox.Vou usar o termo
Overall Directorypara 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
Crie um
.envarquivo no seu diretório principal. DefinaFACEBOOK_ID,VONAGE_API_KEY, eVONAGE_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-SECRETCopiarVocê 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 channelsaba na página do seu aplicativo.No diretório do seu projeto, vá para
settings.py, carregue as variáveis do seu arquivo .env usandopython-dotenvinstalado a partir derequirements.txt. Adicione o seguinte trecho emsettings.pypara carregar o arquivo .env:from dotenv import load_dotenv import os load_dotenv()Copiar
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"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.
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.usernameCriamos 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 messagesVamos 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 responseComo 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, chamamossave_messageque salva a mensagem em nosso banco de dados. Em seguida, enviamos a mensagem como uma mensagem direta do Facebook para o cliente em potencial, chamandosend_outbound. A mensagem é então reenviada para a sala de conversa. Ao final doreceivemé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 emreceivesend_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>
</htmlAntes 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.
Execute
redis-serverpara iniciar o Redis. Você pode parar o servidor Redis com segurança executandoredis-cli shutdownCrie 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.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.ioAdicionar
HOSTdo arquivo .env paraALLOWED_HOSTemsettings.py.ALLOWED_HOSTdefinição emsettings.pydeve ficar assim:ALLOWED_HOSTS = [os.getenv('HOST'), "localhost", "127.0.0.1"]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/inbounde 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 URLeStatus URLcampos.Agora, acesse seu terminal (certifique-se de estar no diretório raiz). Em seguida, execute
python manage.py runserverpara 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:
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.