Initial project import

This commit is contained in:
Tiago
2026-09-29 12:12:31 -03:00
commit c435bb5e23
120 changed files with 5371 additions and 0 deletions
@@ -0,0 +1,219 @@
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 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) {}
}