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 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 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 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 resp = sendNoBody("GET", url, globalApiKey); Map 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 logout(String baseUrl, String instanceName) throws IOException, InterruptedException { return instanceAction("DELETE", baseUrl, "/instance/logout/" + instanceName, instanceName); } public Map excluirInstancia(String baseUrl, String instanceName) throws IOException, InterruptedException { return instanceAction("DELETE", baseUrl, "/instance/delete/" + instanceName, instanceName); } public Map reiniciar(String baseUrl, String instanceName) throws IOException, InterruptedException { return instanceAction("POST", baseUrl, "/instance/restart/" + instanceName, instanceName); } public Map 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 resp = sendNoBody("GET", clean(baseUrl) + "/instance/connectionState/" + instanceName, globalApiKey); Map 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 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 resp = sendNoBody(method, clean(baseUrl) + path, globalApiKey); return Map.of("instance_name", instanceName, "ok", true, "raw", resp.body()); } private HttpResponse 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 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 headers = new LinkedHashMap<>(); if (webhookSecret != null && !webhookSecret.isBlank()) headers.put("X-Webhook-Secret", webhookSecret); Map 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 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 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 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 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) {} }