Hoje iremos continuar nossa série de mensagens para o WhatsApp, mostrando como criar um menu interativo (chatbot), onde o usuário irá enviar mensagens pelo WhatsApp e pelo Protheus iremos interpretar e mandar respostas.
Primeiramente pessoal, tenha os fontes NETiZAP.prw e zZapSend.prw disponibilizados na primeira semana da série – clique aqui para saber mais.
Basicamente a lógica usada aqui, é pensarmos em ouvirmos sempre o que o usuário está dizendo, interpretar a resposta, e enviar uma nova mensagem. Então a lógica seria mais ou menos a seguinte:
- Criar um job que fique rodando no Protheus a cada x minutos (iremos chamar de zZapMenu)
- Nesse job, iremos consultar todas as requisições que usuários enviaram para o nosso número, através do método RequestsStart
- Então iremos percorrer os números de telefones que nos enviaram requisições
- Se não tivermos começado o atendimento ainda, iremos solicitar o CPF
- Caso o atendimento já tenha sido iniciado, iremos enviar ou a resposta do que o usuário solicitou ou o menu para esperar uma resposta
Ah Daniel, mas existe alguma forma de ficar mais online, sem ser por job que fique rodando a cada x minutos? Sim, é possível você configurar um appserver REST, para que sirva de webhook na comunicação, se for esse o caso, a lógica seria a seguinte:
- Caso não tenha um AppServer REST, veja esse link em como configurar – https://terminaldeinformacao.com/2021/07/19/como-configurar-um-appserver-rest-no-protheus/
- Crie um prw / tlpp com um método GET que irá receber um JSON
- Nesse método, copie o trecho das linhas 89 a 269, fazendo adaptações na lógica
- Adicione a URL desse método criado na API da NETiZAP
- Assim quando qualquer pessoa enviar uma mensagem, a API da NETiZAP automaticamente irá enviar o JSON para o seu método criado em AdvPL
Para que ambos os cenários acima sejam possíveis, temos que criar uma tabela de controle em nosso Protheus, nesse caso eu dei o nome para ela de ZAA. Abaixo os campos dela:
- ZAA_FONE, Caractere, 15, Telefone
- ZAA_PRIMSG, Caractere, 20, Primeira Mensagem
- ZAA_ULTMSG, Caractere, 20, Última Mensagem
- ZAA_CLICOD, Caractere, 6 (mesmo do A1_COD), Código do Cliente
- ZAA_CLILOJ, Caractere, 2 (mesmo do A1_LOJA), Loja do Cliente
- ZAA_FUNCAO, Caractere, 10, Função que disparou a pergunta
- ZAA_PERGUN, Caractere, 10, Código da Pergunta
- ZAA_PERDAT, Data, 8, Data da Pergunta
- ZAA_PERHOR, Caractere, 8, Hora da Pergunta
Obs.: Antes de demonstrar a função desenvolvida, o ideal é usar os métodos QuestionSend() e QuestionSearch(), acabei desenvolvendo manualmente.
Abaixo o código da função desenvolvida:
//Bibliotecas
#Include "TOTVS.ch"
#Include "TopConn.ch"
/*/{Protheus.doc} User Function zZapMenu
Função que verifica mensagens recebidas e envia um menu de perguntas
@type Function
@author Atilio
@since 21/08/2021
@version version
@obs O ideal é agendar a rotina via Scheduler do Protheus
Será necessário criar uma tabela que fará controle das interações pelo Protheus, abaixo os campos e o índice
Tabela:
ZAA - Interações via NETiZAP - Totalmente Compartilhada
Campos:
Campo | Tipo | Tamanho | Descrição
ZAA_FONE | Caractere | 15 | Telefone
ZAA_PRIMSG | Caractere | 20 | Primeira Mensagem
ZAA_ULTMSG | Caractere | 20 | Última Mensagem
ZAA_CLICOD | Caractere | 6 (mesmo do A1_COD) | Código do Cliente
ZAA_CLILOJ | Caractere | 2 (mesmo do A1_LOJA) | Loja do Cliente
ZAA_FUNCAO | Caractere | 10 | Função que disparou a pergunta
ZAA_PERGUN | Caractere | 10 | Código da Pergunta
ZAA_PERDAT | Data | 8 | Data da Pergunta
ZAA_PERHOR | Caractere | 8 | Hora da Pergunta
Índices:
1. ZAA_FILIAL + ZAA_FONE
/*/
User Function zZapMenu()
Local aArea
Local cUser := "Administrador"
Local cPass := MemoRead("\x_temp\teste.txt") //Aqui você pode adaptar para a sua lógica, mas evite deixar senhas chumbadas no código fonte
Local cEmpAux := "99"
Local cFilAux := "01"
Local lContinua := .F.
Private lJobPvt := .F.
//Se não tiver ambiente aberto, é job
If Select("SX2") == 0
//Reseta o ambiente e abre ele novamente
RpcClearEnv()
RpcSetEnv(cEmpAux, cFilAux, cUser, cPass, "FAT")
lJobPvt := .T.
lContinua := .T.
Else
lContinua := MsgYesNo("Deseja executar o processamento da busca das mensagens no WhatsApp?", "Atenção")
EndIf
aArea := GetArea()
//Se for continuar, irá chamar a rotina de processamento
If lContinua
Processa({|| fProcessar() }, "Processando...")
EndIf
RestArea(aArea)
Return
Static Function fProcessar()
Local aArea := GetArea()
Local cLinha := SuperGetMV("MV_X_ZAPLI", .F., "5521986855299")
Local nPorta := SuperGetMV("MV_X_ZAPPO", .F., 13155)
Local cChave := SuperGetMV("MV_X_ZAPCH", .F., "fUzanE5ncxlTAWBjMO30")
Local cMensagens := ""
Local oJSON := JsonObject():New()
Local cRet
Local nTelAtu := 0
Local cTelefone := ""
Local cPriMsg := ""
Local cUltMsg := ""
Local cFuncao := ""
Local cPergun := ""
Local dData := sToD("")
Local cHora := ""
Local cUltPerg := ""
Local cUltResp := ""
Local cZapMsg := ""
Local lErro := .F.
Local cTelCli := ""
//Instancia a classe, e passa os parametros da NETiZAP
oZap := NETiZAP():New(cLinha, cChave, nPorta)
oZap:RequestsStart()
cMensagens := oZap:cResponse
//Se houver mensagens
If ! Empty(cMensagens)
//Transforma o JSON em um Objeto
cRet := oJSON:FromJson(cMensagens)
If ValType(cRet) == "U"
aTelefones := oJSON:GetJsonObject("root")
//Percorre os números de telefones
If ValType(aTelefones) == "A"
For nTelAtu := 1 To Len(aTelefones)
cTelefone := aTelefones[nTelAtu]:GetJsonObject("phone")
cPriMsg := aTelefones[nTelAtu]:GetJsonObject("message_datehour_first")
cUltMsg := aTelefones[nTelAtu]:GetJsonObject("message_datehour_last")
DbSelectArea("ZAA")
ZAA->(DbSetOrder(1)) // ZAA_FILIAL + ZAA_FONE
//Se encontrou na tabela customizada (não vai fazer nada, apenas deixar o registro posicionado)
If ZAA->(DbSeek(FWxFilial("ZAA") + cTelefone))
//Senão, irá incluir um novo registro
Else
RecLock("ZAA", .T.)
ZAA->ZAA_FILIAL := FWxFilial("ZAA")
ZAA->ZAA_FONE := cTelefone
ZAA->ZAA_PRIMSG := cPriMsg
ZAA->ZAA_ULTMSG := ""
ZAA->ZAA_CLICOD := ""
ZAA->ZAA_CLILOJ := ""
ZAA->ZAA_FUNCAO := ""
ZAA->ZAA_PERGUN := ""
ZAA->ZAA_PERDAT := sToD("")
ZAA->ZAA_PERHOR := ""
ZAA->(MsUnlock())
EndIf
//Se o horário de fim estiver diferente, quer dizer que veio novas mensagens
If ZAA->ZAA_ULTMSG != cUltMsg
cFuncao := ""
cPergun := ""
dData := sToD("")
cHora := ""
//Se não houve pergunta ainda ou se a data da última pergunta for diferente da data de hoje, perguntamos sobre o cpf da pessoa
If Empty(ZAA->ZAA_PERGUN) .Or. ZAA->ZAA_PERDAT != Date()
//Dispara a mensagem para buscar o cpf ou cnpj
cPergun := "CPF"
cZapMsg := fMensagem(cPergun)
aZap := u_zZapSend(Alltrim(ZAA->ZAA_FONE), cZapMsg)
//Se houve falha, pula o telefone
If ! aZap[1]
nTelAtu++
Loop
EndIf
//Senão
Else
aMensagens := aTelefones[nTelAtu]:GetJsonObject("messages")
//Armazena a última pergunta em uma variável
cUltPerg := Alltrim(ZAA->ZAA_PERGUN)
//Armazena a resposta em outra variável
cUltResp := aMensagens[Len(aMensagens)]:GetJsonObject("message")
//Se a pergunta foi sobre o CPF
If cUltPerg == "CPF"
//Retira caracteres especiais digitados pelo usuário
cUltResp := StrTran(cUltResp, ' ', '')
cUltResp := StrTran(cUltResp, '.', '')
cUltResp := StrTran(cUltResp, '/', '')
cUltResp := StrTran(cUltResp, '-', '')
DbSelectArea("SA1")
SA1->(DbSetOrder(3)) // A1_FILIAL + A1_CGC
//Busca na SA1 o CPF
If SA1->(DbSeek(FWxFilial('SA1') + cUltResp))
cTelCli := "55" + SubStr(SA1->A1_DDD, 2) + Alltrim(SA1->A1_TEL)
cTelCli := StrTran(cTelCli, "-", "")
cTelCli := StrTran(cTelCli, " ", "")
//Se o DDD e o telefone estiverem diferentes
If cTelCli != Alltrim(ZAA->ZAA_FONE)
lErro := .T.
cPergun := "CPF"
Else
RecLock("ZAA", .F.)
ZAA->ZAA_CLICOD := SA1->A1_COD
ZAA->ZAA_CLILOJ := SA1->A1_LOJA
ZAA->(MsUnlock())
//Dispara a mensagem com o menu
lErro := .F.
cPergun := "MENU"
cZapMsg := fMensagem("MENU")
aZap := u_zZapSend(Alltrim(ZAA->ZAA_FONE), cZapMsg)
//Se houve falha, pula o telefone
If ! aZap[1]
nTelAtu++
Loop
EndIf
EndIf
Else
lErro := .T.
cPergun := "CPF"
EndIf
//Se houve erro, manda mensagem, dizendo que não compreendeu a resposta
If lErro
cZapMsg := fMensagem("ERRO")
aZap := u_zZapSend(Alltrim(ZAA->ZAA_FONE), cZapMsg)
//Se houve falha, pula o telefone
If ! aZap[1]
nTelAtu++
Loop
EndIf
EndIf
//Se a pergunta foi sobre o que ele gostaria de saber
Else
cUltResp := SubStr(cUltResp, 1, 1)
cPergun := ZAA->ZAA_PERGUN
//Se a resposta foi 1 = Consultar valor de títulos em aberto
If cUltResp == "1"
cZapMsg := fMensagem("ABERTO") + "<br>--<br>" + fMensagem("MENU")
//Se a resposta foi 2 = Ver dados de compras
ElseIf cUltResp == "2"
cZapMsg := fMensagem("RESUMO") + "<br>--<br>" + fMensagem("MENU")
//Se a resposta foi 3 = Para qual e-Mail esta enviando as DANFEs
ElseIf cUltResp == "3"
cZapMsg := fMensagem("EMAIL") + "<br>--<br>" + fMensagem("MENU")
//Se a resposta foi 4 = Falar com atendente
ElseIf cUltResp == "4"
cZapMsg := fMensagem("ATENDENTE") + "<br>--<br>" + fMensagem("MENU")
//Se a resposta foi 0 = Sair e cancelar (zera a pergunta para identificar que o usuário foi atendido)
ElseIf cUltResp == "0"
cPergun := ""
cZapMsg := fMensagem("TCHAU")
//Senão, diz que não compreendeu
Else
cZapMsg := fMensagem("ERRO") + "<br><br>" + fMensagem("MENU")
EndIf
//Dispara a mensagem
aZap := u_zZapSend(Alltrim(ZAA->ZAA_FONE), cZapMsg)
//Se houve falha, pula o telefone
If ! aZap[1]
nTelAtu++
Loop
EndIf
EndIf
EndIf
//Atualiza as variáveis
cFuncao := "zZapMenu"
dData := Date()
cHora := Time()
//Grava a informação na tabela
RecLock("ZAA", .F.)
ZAA->ZAA_ULTMSG := cUltMsg
ZAA->ZAA_FUNCAO := cFuncao
ZAA->ZAA_PERGUN := cPergun
ZAA->ZAA_PERDAT := dData
ZAA->ZAA_PERHOR := cHora
ZAA->(MsUnlock())
EndIf
Next
EndIf
EndIf
EndIf
RestArea(aArea)
Return
Static Function fMensagem(cPergunta)
Local cMensagem := ""
Local cPiscada := "\uE405"
Local cSorriSuor := "\uD83D\uDE05"
//Monta a mensagem para perguntar do CPF
If cPergunta == "CPF"
cMensagem := "Olá.<br>"
cMensagem += "Obrigado por entrar em contato conosco " + cPiscada + ".<br>"
cMensagem += "Por favor, nos diga, qual é o seu <b>CPF</b> ou seu <b>CNPJ</b>?"
//Monta a mensagem para exibir o menu principal
ElseIf cPergunta == "MENU"
DbSelectArea("SA1")
SA1->(DbSetOrder(1)) // A1_FILIAL + A1_COD + A1_LOJA
SA1->(DbSeek(FWxFilial('SA1') + ZAA->ZAA_CLICOD + ZAA->ZAA_CLILOJ))
//Se tiver o contato, usa ele, do contrário, usa o nome reduzido
If ! Empty(SA1->A1_CONTATO)
cNome := Alltrim(SA1->A1_CONTATO)
Else
cNome := Alltrim(SA1->A1_NREDUZ)
EndIf
cNome := Capital(cNome)
cMensagem := cNome + ", abaixo nossa lista de opções:<br>"
cMensagem += "<b>1</b> - Consultar valor de títulos em aberto<br>"
cMensagem += "<b>2</b> - Ver resumo de compras<br>"
cMensagem += "<b>3</b> - Para qual e-Mail esta enviando as DANFEs<br>"
cMensagem += "<b>4</b> - Falar com atendente<br><br>"
cMensagem += "<b>0</b> - Sair e cancelar<br><br>"
cMensagem += "Qual é sua resposta?"
//Se a resposta foi 1 = Consultar valor de títulos em aberto
ElseIf cPergunta == "ABERTO"
DbSelectArea("SA1")
SA1->(DbSetOrder(1)) // A1_FILIAL + A1_COD + A1_LOJA
SA1->(DbSeek(FWxFilial('SA1') + ZAA->ZAA_CLICOD + ZAA->ZAA_CLILOJ))
If SA1->A1_SALDUP == 0
cMensagem := "Parabéns, não consta atrasos em nossa base!"
Else
cMensagem := "O saldo em aberto é de <b>R$ " + Alltrim(Transform(SA1->A1_SALDUP, PesqPict('SA1', 'A1_SALDUP'))) + "</b>."
EndIf
//Se a resposta foi 2 = Ver dados de compras
ElseIf cPergunta == "RESUMO"
DbSelectArea("SA1")
SA1->(DbSetOrder(1)) // A1_FILIAL + A1_COD + A1_LOJA
SA1->(DbSeek(FWxFilial('SA1') + ZAA->ZAA_CLICOD + ZAA->ZAA_CLILOJ))
cMensagem := "Valor da Maior Compra: <b>" + Alltrim(Transform(SA1->A1_MCOMPRA, PesqPict('SA1', 'A1_MCOMPRA'))) + "</b><br>"
cMensagem += "Data da Primeira Compra: <b>" + dToC(SA1->A1_PRICOM) + "</b><br>"
cMensagem += "Data da Última Compra: <b>" + dToC(SA1->A1_ULTCOM) + "</b><br>"
cMensagem += "Quantas vezes já comprou: <b>" + Alltrim(Transform(SA1->A1_NROCOM, PesqPict('SA1', 'A1_NROCOM'))) + "</b><br>"
//Se a resposta foi 3 = Para qual e-Mail esta enviando as DANFEs
ElseIf cPergunta == "EMAIL"
DbSelectArea("SA1")
SA1->(DbSetOrder(1)) // A1_FILIAL + A1_COD + A1_LOJA
SA1->(DbSeek(FWxFilial('SA1') + ZAA->ZAA_CLICOD + ZAA->ZAA_CLILOJ))
If ! Empty(SA1->A1_EMAIL)
cMensagem := "As DANFEs estão indo atualmente para o e-Mail cadastrado: <b>" + SA1->A1_EMAIL + "</b>"
Else
cMensagem := "Atualmente não existe nenhum e-mail cadastrado no sistema."
EndIf
//Se a resposta foi 4 = Falar com atendente
ElseIf cPergunta == "ATENDENTE"
cMensagem := "Abaixo os meios de comunicação com um atendente:<br>"
cMensagem += "e-Mail: <b>contato@atiliosistemas.com</b><br>"
cMensagem += "Telefone: <b>(14) 9 9738-5495</b><br>"
cMensagem += "Site: <b>https://atiliosistemas.com/</b>"
//Se for a opção 0, irá se despedir
ElseIf cPergunta == "TCHAU"
cMensagem := "Obrigado por entrar em contato conosco.<br>"
cMensagem += "Até logo."
//Se houve alguma falha
ElseIf cPergunta == "ERRO"
cMensagem := "Desculpe, mas não consegui identificar sua resposta " + cSorriSuor + ", poderia repeti-la por favor?<br>"
cMensagem += "<i>Se possível, não use pontuação ou caracteres especiais</i>"
EndIf
Return cMensagem
Por último, veja o print dos envios das mensagens:
Obs.: Os códigos desenvolvidos nessa série do WhatsApp, estão dentro do nosso GitHub, o link é https://github.com/dan-atilio/AdvPL.
Lembrando também pessoal, se tiverem interesse em adquirir uma licença da API, entrem em contato com o pessoal da NETiZAP clicando aqui, e digam que conhecem o Atilio do Terminal de Informação.
Bom pessoal, por hoje é só.
Abraços e até a próxima.

fala Daniel blz fiz o esquema do REST.
Criei um rdmake separado, mais nao entendi a parte de
* Adicione a URL desse método criado na API da NETiZAP
como ficaria lá no fonte?
Fala Bruno, tudo joia?
Então após você criar seu WS, você vai ter ele em uma URL, por exemplo, http://seuip:suaporta/SeuWSQueFoiCriadoEmAdvPL
Ai no painel administrativo da API da NETiZAP (após você contratar o serviço deles), existe uma configuração, que você fala para ele disparar em um WebHook, e nessa configuração, você coloca essa sua URL que foi criada.
Boa tarde, como vai?
Consegue postar em exemplo deste WS que vc comenta ?
Boa tarde Rogério, tudo joia graças a Deus e você?
Opa, siga o passo a passo nesse link – https://terminaldeinformacao.com/2022/09/15/como-criar-uma-aplicacao-rest-em-advpl-em-poucos-passos/
Só que faça assim
1. Escolha a opção get simples
2. Coloque uma tabela qualquer
3. Depois que o Autumn gerar o código, você pega o conteúdo dentro do método
3.1 Ai adapte com o trecho das linhas 89 a 269 desse link
Um grande abraço.