No artigo de hoje, vou mostrar para vocês como integrar facilmente com WhatsApp no Protheus usando a API da ConnectZap.
Atualização em 2025
Pessoal, foi feito um novo artigo, inclusive com um vídeo atualizado no YouTube demonstrando como integrar o Protheus com a plataforma da ConnectZap.
Segue o link: https://terminaldeinformacao.com/2025/01/17/integrando-o-protheus-com-whatsapp-atraves-da-connectzap/
Artigo original em 2021
Recentemente o grande Jader veio conversar comigo, explicando sobre uma ótima forma de integrar o Protheus com WhatsApp via AdvPL / TL++ usando a API da ConnectZap ( https://www.connectzap.com.br/ ).
Ele preparou dois códigos fontes de exemplo, que já estão prontos para qualquer um usar a API da ConnectZap.
Dos dois fontes, o ConnZap.prw é uma tela de leitura, que faz a integração entre o ConnectZap e o Protheus (também pode ser feito no portal). Já o MsgZap.prw é a função que vai ser acionada para o disparo da mensagem, que recebe 3 parâmetros:
- cCelular – Número de celular que será recebido, por exemplo, 5521992584067
- cMsg – Mensagem que será enviada
- cAnexo – Caminho do arquivo que será enviado como anexo (esse é um parâmetro opcional)
Nesses dois fontes, também é necessário que você crie 2 parâmetros na sua base:
- MV_ZAPURL – URL da ConnectZap
- MV_ZSESSAO – Token / Sessão usado na integração da ConnectZap
Além de tudo isso, o pessoal da ConnectZap disponibilizou um excelente manual com as funcionalidades da API, veja em https://www.connectzap.com.br/manual.pdf.
Abaixo um vídeo de como funciona a integração com o Protheus via AdvPL.
Código fonte completo do ConnZap.prw:
//Bibliotecas #Include 'TOTVS.ch' #Include 'ParmType.ch' /*/{Protheus.doc} User Function ConnZap Tela de leitura do QRCODE - WhatsApp @type Function @author Jader Berto @since 13.08.2019 @version version @example @see https://www.connectzap.com.br/manual.pdf /*/ User Function ConnZap() Local aArea := GetArea() Local oNewPag Local oStepWiz := Nil Local oPanelBkg Private cMsg := Space(50) Private oDlg := Nil Private cNome Private cStatus := "" Private oSay1 Private cGet1 := Space(150) Private cGet2 := Space(14) Private cGet3 := Space(250) //Se a pasta qrcode não existir na Protheus Data, cria If ! ExistDir("\qrcode") MakeDir("\qrcode") EndIf //Busca o Token / Sessão cNome := Alltrim(GetMv("MV_ZSESSAO")) //Criando a dialog da integração oDlg:= MSDialog():New(0, 0, 300, 300, 'Conecte o WhatsApp com seu SmartPhone', , , , nOr(WS_VISIBLE, WS_POPUP), CLR_BLACK, CLR_WHITE, , , .T., , , , .T.) oDlg:nWidth := 600 oDlg:nHeight := 600 //Cria um painel de fundo, e vincula um Wizard a ele oPanelBkg:= tPanel():New(0, 0, "", oDlg, , , , , , 300, 300) oStepWiz:= FWWizardControl():New(oPanelBkg) oStepWiz:ActiveUISteps() //Página 1 do Wizard oNewPag := oStepWiz:AddStep("1") oNewPag:SetStepDescription("Seleção de Token") oNewPag:SetConstruction({|oPanel| Cria_Pg1(oPanel, @cNome)}) oNewPag:SetNextAction({|| Valida_Pg1(@cNome)}) oNewPag:SetCancelAction({||, fCancel()}) //Página 2 do Wizard oNewPag := oStepWiz:AddStep("2", {|oPanel| Cria_Pg2(oPanel)}) oNewPag:SetStepDescription("Leitura de QrCode") oNewPag:SetNextAction({||Valida_Pg2(@cNome)}) oNewPag:SetCancelAction({|| fCancel()}) //Página 3 do Wizard oNewPag := oStepWiz:AddStep("3", {||oDlg:End()}) oNewPag:SetStepDescription("Teste de envio") oNewPag:SetNextAction({||.T.}) //Define o bloco ao clicar no botão Voltar oNewPag:SetCancelAction({|| fCancel()}) oNewPag:SetPrevAction({||, .T.}) oStepWiz:Activate() ACTIVATE DIALOG oDlg CENTER oStepWiz:Destroy() RestArea(aArea) Return //Construção da Página 1 Static Function Cria_Pg1(oPanel, cNome) Local aItems := {} //Monta o say e o combo para definir o Token oSay1:= TSay():New(10, 10, {||'Escolha o seu token de acesso'}, oPanel, , , , , , .T., , , 200, 20) cNome := PADR(cNome, 30) aAdd(aItems, Alltrim(GetMV("MV_ZSESSAO"))) oCombo := tComboBox():new(20, 10, {|u|if(pcount()>0, cNome:=u, cNome)}, aItems, 100, 20, oPanel, , , , , , .t., , , , , , , , , ) Return //Validação da página 1 Static Function Valida_Pg1(cNome) Local cStrJson := "" Local oObj Local lRet := .T. cNome := Alltrim(cNome) Processa({||cStrJson := U_fStatus(cNome)}) //Busca o JSON e deserializa FWJsonDeserialize(cStrJson, @oObj) //Se o objeto estiver nulo, mostra um help, senão busca o status da integração If oObj == Nil Help(" ", 1, "Help", , "O serviço do Whatsapp está desconectado, favor reportar ao administrador!", 1, 0) lRet := .F. Else cStatus := oObj:Status:state EndIf Return lRet //Construção da página 2 Static Function Cria_Pg2(oPanel) Local oTimer Local oObj Local oFont //Define a fonte, e cria um Say oFont := TFont():New('Courier new', , -14, .T.) oSay1 := TSay():New( 10, 10, {|u| if( PCount() > 0, cMsg := u, cMsg ) } , oPanel, , oFont, , , , .T., CLR_BLUE, CLR_WHITE, 300, 009) //Verifica o status da conexão cStrJson := U_fStatus(cNome) FWJsonDeserialize(cStrJson, @oObj) cStatus := oObj:Status:state //Se for não encontrado, inicializa a conexão If cStatus == "NOTFOUND" cStrJson := U_fStart(cNome) Sleep(15000) cStrJson := U_fStatus(cNome) FWJsonDeserialize(cStrJson, @oObj) cStatus := oObj:Status:state EndIf //Se já estiver conectado If cStatus == "CONNECTED" oSay1:SetText('Seu WhatsApp já está conectado!') oTBitmap1 := TBitmap():New(40, 80, 260, 184, , '\qrcode\whatsapp.png', .T., oPanel, {||}, , .F., .F., , , .F., , .T., , .F.) //Senão cria um temporizador, e mostra a mensagem para apontar para o QRCode Else oTimer := TTimer():New(1, {|| BuscaQR(oPanel) }, oDlg ) oTimer:Activate() oSay1:SetText('Abra o WhatsApp Web do seu SmartPhone e aponte a camera para o QRCODE:') EndIf Return //Validação da página 2 Static Function valida_pg2(cNome) Local lRet := .T. //Se o status não estiver conectado If cStatus != "CONNECTED" Help(" ", 1, "Help", , "O serviço do Whatsapp não está sincronizado, faça a leitura do Qrcode!", 1, 0) lRet := .F. EndIf Return lRet Static Function BuscaQR(oPanel) Local cStrJson := "" Local oObj Local cFile Local lRet := .T. //Define o nome do arquivo png do qrcode, e faz a conexão com a API cNome := alltrim(cNome) cFile := "qrcode_" + cNome + ".png" cStrJson := U_fStatus(cNome) FWJsonDeserialize(cStrJson, @oObj) cStatus := oObj:Status:state //Se o status estiver como conectado If cStatus == "CONNECTED" oSay1:SetText('Seu WhatsApp foi conectado!') oTBitmap1 := TBitmap():New(40, 80, 260, 184, , '\qrcode\whatsapp.png', .T., oPanel, {||}, , .F., .F., , , .F., , .T., , .F.) lRet := .T. //Senão, define a imagem do QRCode Else fQrCode(cNome) Sleep(1000) oTBitmap1 := TBitmap():New(40, 80, 260, 184, , '\qrcode\'+cFile, .T., oPanel, {||}, , .F., .F., , , .F., , .T., , .F.) cStrJson := U_fStatus(cNome) lRet := ("CONNECTED" $ cStrJson) EndIf Return lRet // Construção da página 3 Static Function cria_pg3(oPanel, cNome) Local lHasButton := .T. //Get da Mensagem oSay0:= TSay():New(10, 10, {||'Mensagem:'}, oPanel, , , , , , .T., , , 200, 20) TGet():New( 20, 10, { | u | If( PCount() == 0, cGet1, cGet1 := u ) }, oPanel, ; 250, 010, "!@", , 0, 16777215, , .F., , .T., , .F., , .F., .F., , .F., .F. , , "cGet1", , , , lHasButton ) //Get do Número de Celular oSay2:= TSay():New(40, 10, {||'Celular:'}, oPanel, , , , , , .T., , , 70, 20) TGet():New( 50, 10, { | u | If( PCount() == 0, cGet2, cGet2 := u ) }, oPanel, ; 60, 010, "@R (99) 99999-9999", , 0, 16777215, , .F., , .T., , .F., , .F., .F., , .F., .F. , , "cGet2", , , , lHasButton ) //Get do Arquivo (opcional) oSay3:= TSay():New(70, 10, {||'Escolha um arquivo (opcional):'}, oPanel, , , , , , .T., , , 200, 20) TGet():New( 80, 10, { | u | If( PCount() == 0, cGet3, cGet3 := u ) }, oPanel, ; 250, 010, "!@", , 0, 16777215, , .F., , .T., , .F., , .F., .F., , .F., .F. , , "cGet3", , , , lHasButton ) //Botões oTButton1 := TButton():New( 81, 260, "Abrir", oPanel, {||zChooseFile()}, 25, 10, , , .F., .T., .F., , .F., , , .F. ) oTButton2 := TButton():New( 100, 10, "Enviar", oPanel, {||Iif(u_MsgZap('55' + cGet2 , cGet1, cGet3), MSGINFO( "Sucesso!", "Resultado do Envio" ), MSGINFO( "Mensagem não enviada.", "Resultado do Envio" ))}, 25, 10, , , .F., .T., .F., , .F., , , .F. ) Return Static Function zChooseFile() Local aArea := GetArea() Local cDirIni := GetTempPath() Local cTipArq := "Todas extensões (*.*)" Local cTitulo := "Seleção de Arquivos para Processamento" Local lSalvar := .F. Local cArqSel := "" //Se não estiver sendo executado via job If ! IsBlind() //Chama a função para buscar arquivos cArqSel := tFileDialog(; cTipArq, ; // Filtragem de tipos de arquivos que serão selecionados cTitulo, ; // Título da Janela para seleção dos arquivos , ; // Compatibilidade cDirIni, ; // Diretório inicial da busca de arquivos lSalvar, ; // Se for .T., será uma Save Dialog, senão será Open Dialog ; // Se não passar parâmetro, irá pegar apenas 1 arquivo; Se for informado GETF_MULTISELECT será possível pegar mais de 1 arquivo; Se for informado GETF_RETDIRECTORY será possível selecionar o diretório ) If ! Empty(cArqSel) cGet3 := cArqSel EndIf EndIf Return cArqSel //Função que fecha a dialog Static Function fCancel() oDlg:End() Return .T. Static Function fQrCode(cNome) Local cReturn Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms:= "" Local cUrl := SuperGetMV("MV_ZAPURL", .T., "https://api.connectzap.com.br/sistema") Local cSession := Alltrim(cNome) Local cFile := "qrcode_"+alltrim(cNome)+".png" //Realiza a integração e busca o QRCode cUrl += "/QRCode" aAdd(aHeadOut, "User-Agent: Chrome/65.0 (compatible; Protheus " + GetBuild() + ")") aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{"SessionName": "'+cSession+'", "View": true }' cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) //Grava a imagem MemoWrite('\qrcode\'+cFile, cReturn) Return cReturn /*/{Protheus.doc} User Function ConnZap Fecha a conexão da Sessão / Token com ConnectZap @type Function @author Jader Berto @since 13.08.2019 @version version @example @see https://www.connectzap.com.br/manual.pdf /*/ User Function fClose(cNome) Local aArea := GetArea() Local cReturn Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms:= "" Local cUrl := SuperGetMV("MV_ZAPURL", .T., "https://api.connectzap.com.br/sistema") Local cSession := Alltrim(cNome) //Define a operação para fechar e faz a integração cUrl += "/Close" aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{"SessionName":"'+cSession+'"}' cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) RestArea(aArea) Return cReturn /*/{Protheus.doc} User Function ConnZap Inicializa a conexão da Sessão / Token com ConnectZap @type Function @author Jader Berto @since 13.08.2019 @version version @example @see https://www.connectzap.com.br/manual.pdf /*/ User Function fStart(cNome) Local aArea := GetArea() Local cReturn Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms:= "" Local cUrl := SuperGetMV("MV_ZAPURL", .T., "https://api.connectzap.com.br/sistema") Local cSession := Alltrim(cNome) //Define a operação para inicializar e faz a integração cUrl += "/Start" aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{"SessionName":"'+cSession+'"}' cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) Sleep(2000) RestArea(aArea) Return cReturn /*/{Protheus.doc} User Function ConnZap Busca o status da Sessão / Token com ConnectZap @type Function @author Jader Berto @since 13.08.2019 @version version @example @see https://www.connectzap.com.br/manual.pdf /*/ User Function fStatus(cNome) Local aArea := GetArea() Local cReturn Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms:= "" Local cUrl := SuperGetMV("MV_ZAPURL", .T., "https://api.connectzap.com.br/sistema") Local cSession := Alltrim(cNome) //Define a operação para buscar o status e faz a integração cUrl += "/Status" aAdd(aHeadOut, "User-Agent: Chrome/65.0 (compatible; Protheus " + GetBuild() + ")") aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{"SessionName": "'+cSession+'"}' cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) RestArea(aArea) Return cReturn /*/{Protheus.doc} User Function ConnZap Busca informações do dispositivo conectado da Sessão / Token com ConnectZap @type Function @author Jader Berto @since 13.08.2019 @version version @example @see https://www.connectzap.com.br/manual.pdf /*/ User Function fHard(cNome) Local aArea := GetArea() Local cReturn Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms:= "" Local cUrl := SuperGetMV("MV_ZAPURL", .T., "https://api.connectzap.com.br/sistema") Local cSession := Alltrim(cNome) //Define a operação para buscar informações do dispositivo conectado e faz a integração cUrl += "/getHostDevice" aAdd(aHeadOut, "User-Agent: Chrome/65.0 (compatible; Protheus " + GetBuild() + ")") aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{"SessionName": "'+cSession+'"}' cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) RestArea(aArea) Return cReturn
Código fonte completo do MsgZap.prw:
//Bibliotecas #Include 'TOTVS.ch' #Include 'FWMVCDef.ch' #Include 'RestFul.ch' #Include 'FileIO.ch' /*/{Protheus.doc} User Function MsgZap Envio de WHATSAPP via Protheus @type Function @author Jader Berto @since 13.08.2019 @version version @param cCelular, Caractere, Número de Celular que irá receber a mensagem @param cMsg, Caractere, Mensagem que será enviada @param cAnexo, Caractere, Caminho do anexo (parâmetro opcional) @return lRet, Lógico, Retorna se houve sucesso ou não no envio @example Exemplos abaixo: // Com Anexo U_MsgZap('5521992584067' ,'Teste Protheus', 'TSTPOLX01_210505_112556_Administrador.txt') // Sem Anexo U_MsgZap('5521975267383' ,'Teste Protheus') @see https://www.connectzap.com.br/manual.pdf /*/ User Function MsgZap(cCelular, cMsg, cAnexo) Local aArea := GetArea() Local lRet := .F. Default cCelular := "" Default cMsg := "" Default cAnexo := "" Private lAuto := IsBlind() //Retirando caracteres especiais do numero cCelular := Alltrim(cCelular) cCelular := Replace(cCelular, "(", "") cCelular := Replace(cCelular, ")", "") cCelular := Replace(cCelular, "-", "") cCelular := Replace(cCelular, " ", "") //Se o celular tiver vazio, ou o tamanho for menor que 8 If Empty(cCelular) .Or. Len(cCelular) < 8 If lAuto fConOut("CONNECTZAP = Favor enviar o número do celular WhatsApp.") Else MsgInfo("Favor enviar o número do celular WhatsApp.", "Problemas ao enviar" ) EndIf lRet := .F. EndIf //Se não tiver mensagem para enviar If Empty(cMsg) If lAuto fConOut("CONNECTZAP = Favor enviar a mensagem a ser enviada.") Else MsgInfo("Favor enviar a mensagem a ser enviada.", "Problemas ao enviar" ) EndIf lRet := .F. EndIf //Se houver anexo, verifica se a extensão jpg, bmp ou png If ! Empty(cAnexo) If ! (".jpg" $ Lower(cAnexo) .Or. ".bmp" $ Lower(cAnexo) .Or. ".png" $ Lower(cAnexo)) lRet := .F. EndIf EndIf //Se passou pela validação do celular e da mensagem, Faz o envio da mensagem If lRet lRet := SetZap(cCelular, cMsg, cAnexo) EndIf RestArea(aArea) Return lRet Static function SetZap(cCelular ,cMsg, cAnexo) Local oOBJ Local cStrJson Local lRet := .F. Local nTimeOut := 120 Local aHeadOut := {} Local cHeadRet := "" Local cPostParms := "" Local cURL := SuperGetMV("MV_ZAPURL", .T., "http://api.connectzap.com.br/sistema") Local cSession := Alltrim(SuperGetMV("MV_ZSESSAO", .T., "SEUTOKEN")) Local xNome Local nZ := 0 cMsg := EncodeUTF8(Replace(cMsg, Chr(10), "")) cCelular := Alltrim(cCelular) //Se a pasta temporária não existir, cria If ! ExistDir("C:\temp") MakeDir("C:\temp") EndIf //Se a pasta de arquivos não existir, cria If ! ExistDir("C:\temp\files") MakeDir("C:\temp\files") EndIf //Faz a conexão conforme o token do parâmetro cStrJson := U_fStatus(cSession) Sleep(2000) FWJsonDeserialize(cStrJson, @oOBJ) cStatus := oOBJ:Status:state //Se não estiver conectado If cStatus $ "NOTFOUND|CLOSED|" .OR. cStatus == "DISCONNECTED" //Enquanto não estiver checado a informação de QRCode, reinicializa While !("QRCODE" $ U_fStatus(cSession)) cStrJson := U_fStart(cSession) Sleep(3000) //Se estiver conectado, encerra o laço If "CONNECTED" $ cStrJson Exit EndIf EndDo EndIf //Retirando o -enter- da mensagem cMsg := Alltrim(Replace(cMsg, CRLF, "")) //Se houver anexo If ! Empty(cAnexo) cUrl += "/sendFileBase64" aAdd(aHeadOut, 'Accept: application/json') aAdd(aHeadOut, 'Content-Type: application/json') //Se tiver barra no anexo If "/" $ cAnexo xNome := SubStr(cAnexo, RAt("/", cAnexo) + 1, Len(cAnexo)) __CopyFile(cAnexo, "C:\temp\files" + SubStr(cAnexo, RAt("/", cAnexo), Len(cAnexo))) cAnexo := Encode64(, "C:\\temp\\files\\" + SubStr(cAnexo, RAt("/", cAnexo) + 1, Len(cAnexo)), .F., .F.) Else xNome := SubStr(cAnexo, RAt("\", cAnexo) + 1, Len(cAnexo)) __CopyFile(cAnexo, "C:\temp\files" + SubStr(cAnexo, RAt("\", cAnexo), Len(cAnexo))) //Percorre o nome do arquivo, e define \ na última \ For nZ:= 1 to Len(cAnexo) If subs(cAnexo,nZ,1) == '\' nPosIni := nZ + 1 EndIf Next nZ cAnexo := Encode64(,"C:\\temp\\files\\" + SubStr(cAnexo, nPosIni, Len(cAnexo)), .F., .F.) EndIf //Define os parâmetros do Post cPostParms := '{' cPostParms += '"SessionName":"' + cSession + '",' cPostParms += '"phonefull":"' + cCelular + '",' cPostParms += '"base64":"' + cAnexo + '",' cPostParms += '"originalname":"' + xNome + '",' cPostParms += '"caption":"' + cMsg + '"' cPostParms += '}' //Senão, define como mensagem de envio de texto Else cUrl += "/sendText" aAdd(aHeadOut, 'Accept: application/json') aAdd(aHeadOut, 'Content-Type: application/json') cPostParms := '{' cPostParms += '"SessionName":"'+ cSession + '",' cPostParms += '"phonefull":"' + cCelular + '",' cPostParms += '"msg":"' + Alltrim(cMsg) + '"' cPostParms += '}' EndIf //Usa o Post enviando as parametrizações acima cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) sleep(3000) //Se o retorno não for nulo, verifica se o status é conectado ou fechado If cReturn != Nil lRet := (oOBJ:Status:state == "CONNECTED") .OR. (oOBJ:Status:state == "CLOSED") Else lRet := .F. EndIf //Se o retorno não estiver correto If !lRet //Se não for forma automática, mostra a tela de conexão novamente, senão mostra mensagem no console.log If !lAuto u_ConnZap() Else fConOut("CONNECTZAP = Problema ao sincronizar com o Connectzap. Utilize o Painel de integração no protheus ou no site (painel.connectzap.com.br).") EndIf //Realiza um novo envio cReturn := HttpPost(cUrl, , cPostParms, nTimeOut, aHeadOut, @cHeadRet) Sleep(3000) //Verifica se teve sucesso no envio FWJsonDeserialize(cReturn, @oOBJ) lRet := "success" $ cReturn EndIf Return lRet // https://centraldeatendimento.totvs.com/hc/pt-br/articles/360041301114-MP-ADVPL-Como-Ativar-a-fun%C3%A7%C3%A3o-FWLogMsg- Static Function fConOut(cTexto) FWLogMsg("INFO", /*cTransactionId*/, "CONOUT", /*cCategory*/, /*cStep*/, /*cMsgId*/, cTexto, /*nMensure*/, /*nElapseTime*/, /*aMessage*/) Return
Bom pessoal, por hoje é só.
Abraços e até a próxima.