unit ZapApi; { ============================================================================ ZapApi - cliente Delphi para a API de WhatsApp da PluguiZap (rotas /instances/{ID}/token/{TOKEN}) Requisitos - Delphi XE7 ou mais novo (usa System.JSON e System.NetEncoding) - Indy 10 (já vem com o Delphi) - libeay32.dll e ssleay32.dll (OpenSSL 1.0.2) na mesma pasta do .exe (32 bits para exe 32 bits, 64 bits para exe 64 bits) Uso var Zap: TZapApi; R: TZapResposta; begin Zap := TZapApi.Create('SEU_ID', 'SEU_TOKEN', 'SEU_CLIENT_TOKEN'); try R := Zap.EnviarTexto('79999998888', 'Seu pedido saiu para entrega'); if R.Ok then ShowMessage('Enviado: ' + R.MessageId) else ShowMessage('Erro: ' + R.Erro); Zap.EnviarArquivo('79999998888', 'C:\Boletos\boleto-1042.pdf', 'Segue o boleto'); finally Zap.Free; end; end; Observações - O telefone pode ir com ou sem 55; com DDD e 10 ou 11 dígitos o 55 é colocado sozinho. - Cada chamada é síncrona (trava a tela enquanto envia). Em telas, chame dentro de um TTask/TThread ou use um timer para não congelar o formulário. - Não compartilhe uma mesma instância de TZapApi entre threads. ============================================================================ } interface uses System.SysUtils, System.Classes, System.JSON, System.NetEncoding, System.IOUtils, IdHTTP, IdSSLOpenSSL, IdExceptionCore; type TZapResposta = record Ok: Boolean; // True quando a API respondeu 2xx Codigo: Integer; // código HTTP (0 = falha de rede) Json: string; // resposta completa, para quem quiser ler outros campos MessageId: string; // id da mensagem enviada (quando houver) Erro: string; // mensagem de erro legível (quando Ok = False) end; TZapApi = class private FHttp: TIdHTTP; FSsl: TIdSSLIOHandlerSocketOpenSSL; FBase: string; FClientToken: string; function Req(const Metodo, Rota: string; const Corpo: string = ''): TZapResposta; class function Campo(const Json, Nome: string): string; static; class function Tel(const T: string): string; static; class function Js(const S: string): string; static; class function MimeDe(const Ext: string): string; static; public constructor Create(const InstanciaId, Token, ClientToken: string; const UrlBase: string = 'https://api.pluguizap.com.br'); destructor Destroy; override; // Conexão function Status: TZapResposta; function Conectado: Boolean; function QRCode: TZapResposta; // Json: {"connected":false,"value":"data:image/png;base64,..."} function QRCodeBase64: string; // só a imagem PNG em base64 ('' se já conectado) function CodigoTelefone(const Telefone: string): string; function Reiniciar: TZapResposta; function Desconectar: TZapResposta; // Mensagens function EnviarTexto(const Telefone, Mensagem: string): TZapResposta; function EnviarImagem(const Telefone, ImagemUrlOuBase64: string; const Legenda: string = ''): TZapResposta; function EnviarDocumentoUrl(const Telefone, Url, NomeArquivo: string; const Legenda: string = ''): TZapResposta; function EnviarArquivo(const Telefone, CaminhoArquivo: string; const Legenda: string = ''): TZapResposta; function EnviarEnquete(const Telefone, Pergunta: string; const Opcoes: array of string; UmaOpcao: Boolean = True): TZapResposta; function EnviarBotoes(const Telefone, Mensagem: string; const Botoes: array of string): TZapResposta; function EnviarLink(const Telefone, Mensagem, Url: string; const Titulo: string = ''; const Descricao: string = ''): TZapResposta; function EnviarLocalizacao(const Telefone: string; Latitude, Longitude: Double; const Nome: string = ''; const Endereco: string = ''): TZapResposta; function MarcarLida(const Telefone, MessageId: string): TZapResposta; // Contatos function TemWhatsApp(const Telefone: string): Boolean; // Webhook (endereço do seu sistema que recebe as mensagens) function ConfigurarWebhook(const Url: string): TZapResposta; end; implementation { TZapApi } constructor TZapApi.Create(const InstanciaId, Token, ClientToken, UrlBase: string); begin inherited Create; FBase := UrlBase; while (FBase <> '') and (FBase[Length(FBase)] = '/') do Delete(FBase, Length(FBase), 1); FBase := FBase + '/instances/' + InstanciaId + '/token/' + Token; FClientToken := ClientToken; FSsl := TIdSSLIOHandlerSocketOpenSSL.Create(nil); FSsl.SSLOptions.Method := sslvTLSv1_2; FSsl.SSLOptions.SSLVersions := [sslvTLSv1_2]; FHttp := TIdHTTP.Create(nil); FHttp.IOHandler := FSsl; FHttp.HandleRedirects := True; FHttp.ConnectTimeout := 15000; FHttp.ReadTimeout := 60000; FHttp.Request.Accept := 'application/json'; FHttp.Request.UserAgent := 'ZapApi-Delphi/1.0'; end; destructor TZapApi.Destroy; begin FHttp.Free; FSsl.Free; inherited; end; function TZapApi.Req(const Metodo, Rota, Corpo: string): TZapResposta; var Envio: TStringStream; Resp: TStringStream; begin Result.Ok := False; Result.Codigo := 0; Result.Json := ''; Result.MessageId := ''; Result.Erro := ''; Envio := TStringStream.Create(Corpo, TEncoding.UTF8); Resp := TStringStream.Create('', TEncoding.UTF8); try try FHttp.Request.CustomHeaders.Clear; if FClientToken <> '' then FHttp.Request.CustomHeaders.Values['Client-Token'] := FClientToken; FHttp.Request.ContentType := 'application/json'; FHttp.Request.CharSet := 'utf-8'; if Metodo = 'GET' then FHttp.Get(FBase + '/' + Rota, Resp) else if Metodo = 'PUT' then FHttp.Put(FBase + '/' + Rota, Envio, Resp) else FHttp.Post(FBase + '/' + Rota, Envio, Resp); Result.Codigo := FHttp.ResponseCode; Result.Json := Resp.DataString; Result.Ok := (Result.Codigo >= 200) and (Result.Codigo < 300); Result.MessageId := Campo(Result.Json, 'messageId'); except on E: EIdHTTPProtocolException do begin // A API respondeu com erro (400, 401, 402, 404...): o corpo traz o motivo Result.Codigo := E.ErrorCode; Result.Json := E.ErrorMessage; Result.Erro := Campo(Result.Json, 'error'); if Result.Erro = '' then Result.Erro := 'HTTP ' + IntToStr(E.ErrorCode); end; on E: Exception do Result.Erro := 'Falha de conexão: ' + E.Message; end; finally Envio.Free; Resp.Free; end; end; { Lê um campo de primeiro nível da resposta JSON (texto, número ou booleano). } class function TZapApi.Campo(const Json, Nome: string): string; var V: TJSONValue; P: TJSONPair; begin Result := ''; if Json = '' then Exit; V := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetBytes(Json), 0); if V = nil then Exit; try if V is TJSONObject then begin P := TJSONObject(V).Get(Nome); if (P <> nil) and not (P.JsonValue is TJSONNull) then begin if P.JsonValue is TJSONString then Result := TJSONString(P.JsonValue).Value else Result := P.JsonValue.ToString; end; end; finally V.Free; end; end; { Coloca o 55 quando o número vem só com DDD; grupos e JIDs passam direto. } class function TZapApi.Tel(const T: string): string; var I: Integer; begin if (Pos('-group', T) > 0) or (Pos('@', T) > 0) then Exit(T); Result := ''; for I := 1 to Length(T) do if CharInSet(T[I], ['0'..'9']) then Result := Result + T[I]; if (Length(Result) = 10) or (Length(Result) = 11) then Result := '55' + Result; end; { Texto como string JSON: aspas, barras e quebras de linha escapadas. } class function TZapApi.Js(const S: string): string; var I: Integer; C: Char; B: TStringBuilder; begin B := TStringBuilder.Create(Length(S) + 2); try B.Append('"'); for I := 1 to Length(S) do begin C := S[I]; case C of '"': B.Append('\"'); '\': B.Append('\\'); #8: B.Append('\b'); #9: B.Append('\t'); #10: B.Append('\n'); #12: B.Append('\f'); #13: B.Append('\r'); else if Ord(C) < 32 then B.Append('\u' + IntToHex(Ord(C), 4)) else B.Append(C); end; end; B.Append('"'); Result := B.ToString; finally B.Free; end; end; class function TZapApi.MimeDe(const Ext: string): string; var E: string; begin E := LowerCase(Ext); if (E <> '') and (E[1] = '.') then Delete(E, 1, 1); if E = 'pdf' then Result := 'application/pdf' else if (E = 'jpg') or (E = 'jpeg') then Result := 'image/jpeg' else if E = 'png' then Result := 'image/png' else if E = 'webp' then Result := 'image/webp' else if E = 'gif' then Result := 'image/gif' else if E = 'xml' then Result := 'application/xml' else if E = 'txt' then Result := 'text/plain' else if E = 'csv' then Result := 'text/csv' else if E = 'zip' then Result := 'application/zip' else if E = 'doc' then Result := 'application/msword' else if E = 'docx' then Result := 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' else if E = 'xls' then Result := 'application/vnd.ms-excel' else if E = 'xlsx' then Result := 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' else if E = 'mp3' then Result := 'audio/mpeg' else if E = 'ogg' then Result := 'audio/ogg' else if E = 'mp4' then Result := 'video/mp4' else Result := 'application/octet-stream'; end; { ---------- Conexão ---------- } function TZapApi.Status: TZapResposta; begin Result := Req('GET', 'status'); end; function TZapApi.Conectado: Boolean; var R: TZapResposta; begin R := Status; Result := R.Ok and SameText(Campo(R.Json, 'connected'), 'true'); end; function TZapApi.QRCode: TZapResposta; begin Result := Req('GET', 'qr-code/image'); end; function TZapApi.QRCodeBase64: string; var R: TZapResposta; P: Integer; begin Result := ''; R := QRCode; if not R.Ok then Exit; Result := Campo(R.Json, 'value'); P := Pos('base64,', Result); if P > 0 then Result := Copy(Result, P + 7, MaxInt); end; function TZapApi.CodigoTelefone(const Telefone: string): string; var R: TZapResposta; begin R := Req('GET', 'phone-code/' + Tel(Telefone)); if R.Ok then Result := Campo(R.Json, 'value') else Result := ''; end; function TZapApi.Reiniciar: TZapResposta; begin Result := Req('GET', 'restart'); end; function TZapApi.Desconectar: TZapResposta; begin Result := Req('GET', 'disconnect'); end; { ---------- Mensagens ---------- } function TZapApi.EnviarTexto(const Telefone, Mensagem: string): TZapResposta; begin Result := Req('POST', 'send-text', '{"phone":' + Js(Tel(Telefone)) + ',"message":' + Js(Mensagem) + '}'); end; function TZapApi.EnviarImagem(const Telefone, ImagemUrlOuBase64, Legenda: string): TZapResposta; begin Result := Req('POST', 'send-image', '{"phone":' + Js(Tel(Telefone)) + ',"image":' + Js(ImagemUrlOuBase64) + ',"caption":' + Js(Legenda) + '}'); end; function TZapApi.EnviarDocumentoUrl(const Telefone, Url, NomeArquivo, Legenda: string): TZapResposta; var Ext: string; begin Ext := LowerCase(ExtractFileExt(NomeArquivo)); if Ext = '' then Ext := '.pdf'; Delete(Ext, 1, 1); Result := Req('POST', 'send-document/' + Ext, '{"phone":' + Js(Tel(Telefone)) + ',"document":' + Js(Url) + ',"fileName":' + Js(NomeArquivo) + ',"caption":' + Js(Legenda) + '}'); end; { Envia um arquivo do disco (boleto, nota, foto). Imagens vão como imagem; o resto como documento. } function TZapApi.EnviarArquivo(const Telefone, CaminhoArquivo, Legenda: string): TZapResposta; var Bytes: TBytes; B64, Mime, DataUrl: string; begin if not FileExists(CaminhoArquivo) then begin Result.Ok := False; Result.Codigo := 0; Result.Json := ''; Result.MessageId := ''; Result.Erro := 'Arquivo não encontrado: ' + CaminhoArquivo; Exit; end; Bytes := TFile.ReadAllBytes(CaminhoArquivo); B64 := TNetEncoding.Base64.EncodeBytesToString(Bytes); B64 := StringReplace(B64, #13, '', [rfReplaceAll]); B64 := StringReplace(B64, #10, '', [rfReplaceAll]); Mime := MimeDe(ExtractFileExt(CaminhoArquivo)); DataUrl := 'data:' + Mime + ';base64,' + B64; if Pos('image/', Mime) = 1 then Result := EnviarImagem(Telefone, DataUrl, Legenda) else Result := EnviarDocumentoUrl(Telefone, DataUrl, ExtractFileName(CaminhoArquivo), Legenda); end; function TZapApi.EnviarEnquete(const Telefone, Pergunta: string; const Opcoes: array of string; UmaOpcao: Boolean): TZapResposta; var I: Integer; Lista: string; begin Lista := ''; for I := Low(Opcoes) to High(Opcoes) do begin if Lista <> '' then Lista := Lista + ','; Lista := Lista + '{"name":' + Js(Opcoes[I]) + '}'; end; if UmaOpcao then Result := Req('POST', 'send-poll', '{"phone":' + Js(Tel(Telefone)) + ',"message":' + Js(Pergunta) + ',"poll":[' + Lista + '],"pollMaxOptions":1}') else Result := Req('POST', 'send-poll', '{"phone":' + Js(Tel(Telefone)) + ',"message":' + Js(Pergunta) + ',"poll":[' + Lista + ']}'); end; { Chega como menu numerado; a escolha volta no webhook como buttonsResponseMessage com o buttonId = posição (1, 2, 3...). } function TZapApi.EnviarBotoes(const Telefone, Mensagem: string; const Botoes: array of string): TZapResposta; var I: Integer; Lista: string; begin Lista := ''; for I := Low(Botoes) to High(Botoes) do begin if Lista <> '' then Lista := Lista + ','; Lista := Lista + '{"id":' + Js(IntToStr(I - Low(Botoes) + 1)) + ',"label":' + Js(Botoes[I]) + '}'; end; Result := Req('POST', 'send-button-list', '{"phone":' + Js(Tel(Telefone)) + ',"message":' + Js(Mensagem) + ',"buttonList":{"buttons":[' + Lista + ']}}'); end; function TZapApi.EnviarLink(const Telefone, Mensagem, Url, Titulo, Descricao: string): TZapResposta; begin Result := Req('POST', 'send-link', '{"phone":' + Js(Tel(Telefone)) + ',"message":' + Js(Mensagem) + ',"linkUrl":' + Js(Url) + ',"title":' + Js(Titulo) + ',"linkDescription":' + Js(Descricao) + '}'); end; function TZapApi.EnviarLocalizacao(const Telefone: string; Latitude, Longitude: Double; const Nome, Endereco: string): TZapResposta; var Fmt: TFormatSettings; begin Fmt := TFormatSettings.Create; Fmt.DecimalSeparator := '.'; Result := Req('POST', 'send-location', '{"phone":' + Js(Tel(Telefone)) + ',"latitude":' + FloatToStr(Latitude, Fmt) + ',"longitude":' + FloatToStr(Longitude, Fmt) + ',"title":' + Js(Nome) + ',"address":' + Js(Endereco) + '}'); end; function TZapApi.MarcarLida(const Telefone, MessageId: string): TZapResposta; begin Result := Req('POST', 'read-message', '{"phone":' + Js(Tel(Telefone)) + ',"messageId":' + Js(MessageId) + '}'); end; { ---------- Contatos ---------- } function TZapApi.TemWhatsApp(const Telefone: string): Boolean; var R: TZapResposta; begin R := Req('GET', 'phone-exists/' + Tel(Telefone)); Result := R.Ok and SameText(Campo(R.Json, 'exists'), 'true'); end; { ---------- Webhook ---------- } function TZapApi.ConfigurarWebhook(const Url: string): TZapResposta; begin Result := Req('PUT', 'update-every-webhooks', '{"value":' + Js(Url) + '}'); end; end.