250 lines
12 KiB
Java
250 lines
12 KiB
Java
package com.seconectai.condocontab.integracao;
|
|
|
|
import com.seconectai.condocontab.config.AppConfig;
|
|
import com.seconectai.condocontab.util.Json;
|
|
import com.google.zxing.BarcodeFormat;
|
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
|
import com.google.zxing.qrcode.QRCodeWriter;
|
|
|
|
import java.io.IOException;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.util.Base64;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
|
|
public final class EvolutionApiClient {
|
|
private final String globalApiKey;
|
|
private final String defaultBaseUrl;
|
|
private final HttpClient httpClient;
|
|
|
|
public EvolutionApiClient(AppConfig config) {
|
|
this.globalApiKey = config.evolutionGlobalApiKey();
|
|
this.defaultBaseUrl = config.evolutionApiUrl();
|
|
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
|
}
|
|
|
|
public CriacaoInstancia criarInstancia(String baseUrl, String instanceName, String token, String numero) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl)) {
|
|
String qr = Base64.getEncoder().encodeToString(("QR:" + instanceName).getBytes(StandardCharsets.UTF_8));
|
|
return new CriacaoInstancia(instanceName, token, qr, true);
|
|
}
|
|
Map<String, Object> payload = new LinkedHashMap<>();
|
|
payload.put("instanceName", instanceName);
|
|
payload.put("integration", "WHATSAPP-BAILEYS");
|
|
payload.put("qrcode", true);
|
|
payload.put("number", onlyDigits(numero));
|
|
HttpRequest req = HttpRequest.newBuilder(URI.create(clean(baseUrl) + "/instance/create"))
|
|
.timeout(Duration.ofSeconds(20))
|
|
.header("Content-Type", "application/json")
|
|
.header("apikey", globalApiKey)
|
|
.POST(HttpRequest.BodyPublishers.ofString(Json.write(payload)))
|
|
.build();
|
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
|
|
throw new IOException("Evolution API retornou HTTP " + resp.statusCode() + ": " + resp.body());
|
|
}
|
|
return new CriacaoInstancia(instanceName, firstNonBlank(extractJsonString(resp.body(), "hash"), token), qrBase64(resp.body()), false);
|
|
}
|
|
|
|
|
|
public Map<String, Object> conectar(String baseUrl, String instanceName, String numero) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl)) {
|
|
String qr = Base64.getEncoder().encodeToString(("QR:" + instanceName).getBytes(StandardCharsets.UTF_8));
|
|
return Map.of("instance_name", instanceName, "qrcode_base64", qr, "mock", true);
|
|
}
|
|
String url = clean(baseUrl) + "/instance/connect/" + instanceName;
|
|
String digits = onlyDigits(numero);
|
|
if (!digits.isBlank()) {
|
|
url += "?number=" + digits;
|
|
}
|
|
HttpResponse<String> resp = sendNoBody("GET", url, globalApiKey);
|
|
Map<String, Object> out = new LinkedHashMap<>();
|
|
out.put("instance_name", instanceName);
|
|
out.put("pairing_code", extractJsonString(resp.body(), "pairingCode"));
|
|
out.put("qr_code_payload", extractJsonString(resp.body(), "code"));
|
|
out.put("qr_code_base64", qrBase64(resp.body()));
|
|
out.put("raw", resp.body());
|
|
return out;
|
|
}
|
|
|
|
public Map<String, Object> logout(String baseUrl, String instanceName) throws IOException, InterruptedException {
|
|
return instanceAction("DELETE", baseUrl, "/instance/logout/" + instanceName, instanceName);
|
|
}
|
|
|
|
public Map<String, Object> excluirInstancia(String baseUrl, String instanceName) throws IOException, InterruptedException {
|
|
return instanceAction("DELETE", baseUrl, "/instance/delete/" + instanceName, instanceName);
|
|
}
|
|
|
|
public Map<String, Object> reiniciar(String baseUrl, String instanceName) throws IOException, InterruptedException {
|
|
return instanceAction("POST", baseUrl, "/instance/restart/" + instanceName, instanceName);
|
|
}
|
|
|
|
public Map<String, Object> estado(String baseUrl, String instanceName) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl)) {
|
|
return Map.of("instance_name", instanceName, "state", "mock");
|
|
}
|
|
HttpResponse<String> resp = sendNoBody("GET", clean(baseUrl) + "/instance/connectionState/" + instanceName, globalApiKey);
|
|
Map<String, Object> out = new LinkedHashMap<>();
|
|
out.put("instance_name", instanceName);
|
|
out.put("state", firstNonBlank(extractJsonString(resp.body(), "state"), extractJsonString(resp.body(), "connectionStatus")));
|
|
out.put("raw", resp.body());
|
|
return out;
|
|
}
|
|
|
|
private Map<String, Object> instanceAction(String method, String baseUrl, String path, String instanceName) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl)) {
|
|
return Map.of("instance_name", instanceName, "ok", true, "mock", true);
|
|
}
|
|
HttpResponse<String> resp = sendNoBody(method, clean(baseUrl) + path, globalApiKey);
|
|
return Map.of("instance_name", instanceName, "ok", true, "raw", resp.body());
|
|
}
|
|
|
|
private HttpResponse<String> sendNoBody(String method, String url, String apiKey) throws IOException, InterruptedException {
|
|
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
|
|
.timeout(Duration.ofSeconds(20))
|
|
.header("apikey", apiKey);
|
|
if ("GET".equals(method)) {
|
|
builder.GET();
|
|
} else if ("DELETE".equals(method)) {
|
|
builder.DELETE();
|
|
} else {
|
|
builder.method(method, HttpRequest.BodyPublishers.noBody());
|
|
}
|
|
HttpResponse<String> resp = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
|
|
throw new IOException("Evolution API retornou HTTP " + resp.statusCode() + ": " + resp.body());
|
|
}
|
|
return resp;
|
|
}
|
|
|
|
public void configurarWebhookRespostas(String baseUrl, String instanceName, String webhookUrl, String webhookSecret) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl) || webhookUrl == null || webhookUrl.isBlank()) return;
|
|
Map<String, Object> headers = new LinkedHashMap<>();
|
|
if (webhookSecret != null && !webhookSecret.isBlank()) headers.put("X-Webhook-Secret", webhookSecret);
|
|
Map<String, Object> webhook = new LinkedHashMap<>();
|
|
webhook.put("enabled", true);
|
|
webhook.put("url", webhookUrl);
|
|
webhook.put("webhook_by_events", false);
|
|
webhook.put("webhook_base64", false);
|
|
webhook.put("byEvents", false);
|
|
webhook.put("base64", false);
|
|
webhook.put("events", java.util.List.of("MESSAGES_UPSERT"));
|
|
if (!headers.isEmpty()) webhook.put("headers", headers);
|
|
Map<String, Object> payload = new LinkedHashMap<>();
|
|
payload.put("webhook", webhook);
|
|
HttpRequest req = HttpRequest.newBuilder(URI.create(clean(baseUrl) + "/webhook/set/" + instanceName))
|
|
.timeout(Duration.ofSeconds(20))
|
|
.header("Content-Type", "application/json")
|
|
.header("apikey", globalApiKey)
|
|
.POST(HttpRequest.BodyPublishers.ofString(Json.write(payload)))
|
|
.build();
|
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
|
|
throw new IOException("Evolution API retornou HTTP " + resp.statusCode() + ": " + resp.body());
|
|
}
|
|
}
|
|
|
|
public String enviarTexto(String baseUrl, String instanceName, String token, String numero, String texto) throws IOException, InterruptedException {
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
baseUrl = defaultBaseUrl;
|
|
}
|
|
if (mockMode(baseUrl)) return "mock";
|
|
Map<String, Object> payload = new LinkedHashMap<>();
|
|
payload.put("number", onlyDigits(numero));
|
|
payload.put("text", texto);
|
|
HttpRequest req = HttpRequest.newBuilder(URI.create(clean(baseUrl) + "/message/sendText/" + instanceName))
|
|
.timeout(Duration.ofSeconds(20))
|
|
.header("Content-Type", "application/json")
|
|
.header("apikey", globalApiKey)
|
|
.POST(HttpRequest.BodyPublishers.ofString(Json.write(payload)))
|
|
.build();
|
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
|
|
throw new IOException("Evolution API retornou HTTP " + resp.statusCode() + ": " + resp.body());
|
|
}
|
|
return resp.body();
|
|
}
|
|
|
|
private boolean mockMode(String baseUrl) {
|
|
return globalApiKey == null || globalApiKey.isBlank() || baseUrl == null || baseUrl.isBlank() || baseUrl.startsWith("mock://");
|
|
}
|
|
|
|
private String clean(String baseUrl) {
|
|
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
|
}
|
|
|
|
public static String onlyDigits(String raw) {
|
|
return raw == null ? "" : raw.replaceAll("\\D", "");
|
|
}
|
|
|
|
private String firstNonBlank(String first, String fallback) {
|
|
return first == null || first.isBlank() ? fallback : first;
|
|
}
|
|
|
|
private String qrBase64(String responseBody) throws IOException {
|
|
String image = firstNonBlank(extractJsonString(responseBody, "base64"), extractJsonString(responseBody, "qrcode"));
|
|
if (looksLikeImageBase64(image)) {
|
|
return stripDataImagePrefix(image);
|
|
}
|
|
String payload = extractJsonString(responseBody, "code");
|
|
if (payload == null || payload.isBlank()) {
|
|
return null;
|
|
}
|
|
try {
|
|
var matrix = new QRCodeWriter().encode(payload, BarcodeFormat.QR_CODE, 320, 320);
|
|
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
MatrixToImageWriter.writeToStream(matrix, "PNG", out);
|
|
return Base64.getEncoder().encodeToString(out.toByteArray());
|
|
}
|
|
} catch (Exception e) {
|
|
throw new IOException("Falha ao gerar QR Code local", e);
|
|
}
|
|
}
|
|
|
|
private boolean looksLikeImageBase64(String value) {
|
|
if (value == null || value.isBlank()) return false;
|
|
String clean = stripDataImagePrefix(value);
|
|
return clean.startsWith("iVBOR") || clean.startsWith("/9j/") || clean.startsWith("PHN2Zy");
|
|
}
|
|
|
|
private String stripDataImagePrefix(String value) {
|
|
if (value == null) return null;
|
|
int comma = value.indexOf(',');
|
|
return value.startsWith("data:image/") && comma > 0 ? value.substring(comma + 1) : value;
|
|
}
|
|
|
|
private String extractJsonString(String raw, String key) {
|
|
String marker = "\"" + key + "\"";
|
|
int p = raw.indexOf(marker);
|
|
if (p < 0) return null;
|
|
int colon = raw.indexOf(':', p);
|
|
int first = raw.indexOf('"', colon + 1);
|
|
int second = raw.indexOf('"', first + 1);
|
|
if (first < 0 || second < 0) return null;
|
|
return raw.substring(first + 1, second);
|
|
}
|
|
|
|
public record CriacaoInstancia(String instanceName, String token, String qrcodeBase64, boolean mock) {}
|
|
}
|