From 21bbc8d38591d4faa0ed5781c7969e438ede6f8d Mon Sep 17 00:00:00 2001 From: Tiago Date: Tue, 29 Sep 2026 17:30:03 -0300 Subject: [PATCH] Track Evolution identifiers for bridge replies --- .../integracao/EvolutionApiClient.java | 18 ++++--- .../integracao/ExternalBridgeHandler.java | 49 +++++++++++-------- .../021_integracao_rastreamento_evolution.sql | 24 +++++++++ 3 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/migrations/master/021_integracao_rastreamento_evolution.sql diff --git a/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java b/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java index 696860c..c73f1b7 100644 --- a/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java +++ b/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java @@ -142,15 +142,17 @@ public final class EvolutionApiClient { 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("enabled", true); - payload.put("url", webhookUrl); - payload.put("webhook_by_events", false); - payload.put("webhook_base64", false); - payload.put("byEvents", false); - payload.put("base64", false); - payload.put("events", java.util.List.of("MESSAGES_UPSERT")); - if (!headers.isEmpty()) payload.put("headers", headers); + payload.put("webhook", webhook); HttpRequest req = HttpRequest.newBuilder(URI.create(clean(baseUrl) + "/webhook/set/" + instanceName)) .timeout(Duration.ofSeconds(20)) .header("Content-Type", "application/json") diff --git a/src/main/java/com/seconectai/condocontab/integracao/ExternalBridgeHandler.java b/src/main/java/com/seconectai/condocontab/integracao/ExternalBridgeHandler.java index 6837a8f..523f0e9 100644 --- a/src/main/java/com/seconectai/condocontab/integracao/ExternalBridgeHandler.java +++ b/src/main/java/com/seconectai/condocontab/integracao/ExternalBridgeHandler.java @@ -91,10 +91,10 @@ public final class ExternalBridgeHandler implements HttpHandler { try { if (telefone.length() < 12) throw new IllegalArgumentException("telefone_invalido"); String resp = evolutionApiClient.enviarTexto(str(inst.get("instance_url")), parts[0], parts.length > 1 ? parts[1] : "", telefone, mensagem); - long msgId = registrarMensagem(m, empresaId, cfg, b.get("referencia_externa"), d, telefone, mensagem, "enviado", resp); + long msgId = registrarMensagem(m, empresaId, cfg, b.get("referencia_externa"), d, telefone, mensagem, "enviado", resp, extractValue(resp, "id"), extractValue(resp, "remoteJid"), extractValue(resp, "remoteJidAlt")); row.put("status", "enviado"); row.put("mensagem_id", msgId); } catch (Exception err) { - long msgId = registrarMensagem(m, empresaId, cfg, b.get("referencia_externa"), d, telefone, mensagem, "falhou", err.getMessage()); + long msgId = registrarMensagem(m, empresaId, cfg, b.get("referencia_externa"), d, telefone, mensagem, "falhou", err.getMessage(), null, null, null); row.put("status", "falhou"); row.put("erro", err.getMessage()); row.put("mensagem_id", msgId); } resultados.add(row); @@ -106,28 +106,33 @@ public final class ExternalBridgeHandler implements HttpHandler { private void receberRespostaEvolution(HttpExchange ex) throws Exception { validarWebhookEvolution(ex); String raw = rawBody(ex); - if (isFromMe(raw)) { - HttpResponses.json(ex, 200, Map.of("ok", true, "encaminhado", false, "motivo", "mensagem_enviada_pelo_sistema")); - return; - } - String numero = telefoneWhatsapp(firstNonBlank(extract(raw, "remoteJidAlt"), extract(raw, "remoteJid"), extract(raw, "from"), extract(raw, "number"), extract(raw, "sender"))); - String texto = firstNonBlank(extract(raw, "conversation"), extract(raw, "text"), extract(raw, "body"), extract(raw, "message")); - if (numero.isBlank()) { - System.err.println("Webhook Evolution sem numero identificavel. Payload inicial: " + raw.substring(0, Math.min(raw.length(), 500))); - HttpResponses.json(ex, 400, Map.of("erro", "numero_nao_identificado")); - return; - } + String remoteJid = extractValue(raw, "remoteJid"); + String remoteJidAlt = extractValue(raw, "remoteJidAlt"); + String messageId = extractValue(raw, "id"); + String numero = telefoneWhatsapp(firstNonBlank(remoteJidAlt, remoteJid, extractValue(raw, "from"), extractValue(raw, "number"), extractValue(raw, "sender"))); + String texto = firstNonBlank(extractValue(raw, "conversation"), extractValue(raw, "text"), extractValue(raw, "body"), extractValue(raw, "message")); try (Connection c = masterDs.getMasterConnection()) { - Map envio = one(c, "SELECT * FROM integracao_mensagens WHERE telefone=? AND callback_url IS NOT NULL AND status='enviado' ORDER BY criado_em DESC,id DESC LIMIT 1", numero); - if (envio == null) { - envio = one(c, "SELECT * FROM integracao_mensagens WHERE telefone=? AND callback_url IS NOT NULL AND criado_em >= now() - interval '24 hours' ORDER BY criado_em DESC,id DESC LIMIT 1", numero); + if (isFromMe(raw)) { + registrarEventoWebhook(c, raw, numero, remoteJid, remoteJidAlt, messageId, texto, null, "ignorado_from_me"); + HttpResponses.json(ex, 200, Map.of("ok", true, "encaminhado", false, "motivo", "mensagem_enviada_pelo_sistema")); + return; } + if (numero.isBlank() && remoteJid == null && remoteJidAlt == null) { + registrarEventoWebhook(c, raw, numero, remoteJid, remoteJidAlt, messageId, texto, null, "numero_nao_identificado"); + System.err.println("Webhook Evolution sem numero identificavel. Payload inicial: " + raw.substring(0, Math.min(raw.length(), 500))); + HttpResponses.json(ex, 400, Map.of("erro", "numero_nao_identificado")); + return; + } + Map envio = buscarEnvioResposta(c, numero, remoteJid, remoteJidAlt); if (envio == null) { - System.out.println("Webhook Evolution sem envio correspondente. numero=" + numero + ", texto=" + resumo(texto)); + registrarEventoWebhook(c, raw, numero, remoteJid, remoteJidAlt, messageId, texto, null, "envio_nao_encontrado"); + System.out.println("Webhook Evolution sem envio correspondente. numero=" + numero + ", remoteJid=" + resumo(remoteJid) + ", remoteJidAlt=" + resumo(remoteJidAlt) + ", texto=" + resumo(texto)); HttpResponses.json(ex, 200, Map.of("ok", true, "encaminhado", false, "motivo", "envio_nao_encontrado")); return; } - long respostaId = registrarResposta(c, ((Number)envio.get("id")).longValue(), raw, texto); + long mensagemId = ((Number)envio.get("id")).longValue(); + long respostaId = registrarResposta(c, mensagemId, raw, texto); + registrarEventoWebhook(c, raw, numero, remoteJid, remoteJidAlt, messageId, texto, mensagemId, "resposta_registrada"); boolean encaminhado = encaminharCallback(envio, respostaId, numero, texto); System.out.println("Webhook Evolution resposta registrada. mensagem_id=" + envio.get("id") + ", resposta_id=" + respostaId + ", numero=" + numero + ", encaminhado=" + encaminhado); HttpResponses.json(ex, 200, Map.of("ok", true, "encaminhado", encaminhado, "mensagem_id", envio.get("id"), "resposta_id", respostaId)); @@ -253,8 +258,10 @@ public final class ExternalBridgeHandler implements HttpHandler { private List dests(Connection c, String sql, String tipo, Object... params) throws SQLException {List out=new ArrayList<>();try(PreparedStatement ps=c.prepareStatement(sql)){bind(ps,params);try(ResultSet rs=ps.executeQuery()){while(rs.next())out.add(new Destinatario(tipo,rs.getLong("id"),rs.getString("nome"),rs.getString("telefone")));}}return out;} private Destinatario dest(Connection c, String sql, String tipo, Object... params) throws SQLException {try(PreparedStatement ps=c.prepareStatement(sql)){bind(ps,params);try(ResultSet rs=ps.executeQuery()){return rs.next()?new Destinatario(tipo,rs.getLong("id"),rs.getString("nome"),rs.getString("telefone")):null;}}} private void addIfPresent(List out, Destinatario d){if(d!=null)out.add(d);} - private long registrarMensagem(Connection c,long emp,ApiKeyConfig cfg,String ref,Destinatario d,String tel,String msg,String status,String detalhe)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO integracao_mensagens (empresa_id,sistema_externo,referencia_externa,callback_url,callback_secret,tipo_destinatario,destinatario_id,destinatario_nome,telefone,mensagem,status,evolution_response) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) RETURNING id")){ps.setLong(1,emp);ps.setString(2,cfg.sistemaExterno());ps.setString(3,ref);ps.setString(4,emptyToNull(cfg.callbackUrl()));ps.setString(5,emptyToNull(cfg.callbackSecret()));ps.setString(6,d.tipo());ps.setLong(7,d.id());ps.setString(8,d.nome());ps.setString(9,tel);ps.setString(10,msg);ps.setString(11,status);ps.setString(12,detalhe);try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}} + private long registrarMensagem(Connection c,long emp,ApiKeyConfig cfg,String ref,Destinatario d,String tel,String msg,String status,String detalhe,String evolutionMessageId,String remoteJid,String remoteJidAlt)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO integracao_mensagens (empresa_id,sistema_externo,referencia_externa,callback_url,callback_secret,tipo_destinatario,destinatario_id,destinatario_nome,telefone,mensagem,status,evolution_response,evolution_message_id,evolution_remote_jid,evolution_remote_jid_alt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) RETURNING id")){ps.setLong(1,emp);ps.setString(2,cfg.sistemaExterno());ps.setString(3,ref);ps.setString(4,emptyToNull(cfg.callbackUrl()));ps.setString(5,emptyToNull(cfg.callbackSecret()));ps.setString(6,d.tipo());ps.setLong(7,d.id());ps.setString(8,d.nome());ps.setString(9,tel);ps.setString(10,msg);ps.setString(11,status);ps.setString(12,detalhe);ps.setString(13,emptyToNull(evolutionMessageId));ps.setString(14,emptyToNull(remoteJid));ps.setString(15,emptyToNull(remoteJidAlt));try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}} private long registrarResposta(Connection c,long msgId,String raw,String texto)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO integracao_respostas (integracao_mensagem_id,resposta_texto,payload) VALUES (?,?,?) RETURNING id")){ps.setLong(1,msgId);ps.setString(2,texto);ps.setString(3,raw);try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}} + private Map buscarEnvioResposta(Connection c,String numero,String remoteJid,String remoteJidAlt)throws SQLException{Map envio=null;if(!isBlank(remoteJid)||!isBlank(remoteJidAlt)){envio=one(c,"SELECT * FROM integracao_mensagens WHERE callback_url IS NOT NULL AND status='enviado' AND (evolution_remote_jid IN (?,?) OR evolution_remote_jid_alt IN (?,?)) ORDER BY criado_em DESC,id DESC LIMIT 1",remoteJid,remoteJidAlt,remoteJid,remoteJidAlt);}if(envio==null&&!isBlank(numero)){envio=one(c,"SELECT * FROM integracao_mensagens WHERE telefone=? AND callback_url IS NOT NULL AND status='enviado' ORDER BY criado_em DESC,id DESC LIMIT 1",numero);}if(envio==null&&!isBlank(numero)){envio=one(c,"SELECT * FROM integracao_mensagens WHERE telefone=? AND callback_url IS NOT NULL AND criado_em >= now() - interval '24 hours' ORDER BY criado_em DESC,id DESC LIMIT 1",numero);}return envio;} + private void registrarEventoWebhook(Connection c,String raw,String numero,String remoteJid,String remoteJidAlt,String messageId,String texto,Long mensagemId,String status)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO integracao_webhook_eventos (telefone,remote_jid,remote_jid_alt,message_id,texto,integracao_mensagem_id,status,payload) VALUES (?,?,?,?,?,?,?,?)")){ps.setString(1,emptyToNull(numero));ps.setString(2,emptyToNull(remoteJid));ps.setString(3,emptyToNull(remoteJidAlt));ps.setString(4,emptyToNull(messageId));ps.setString(5,texto);if(mensagemId==null)ps.setNull(6,Types.BIGINT);else ps.setLong(6,mensagemId);ps.setString(7,status);ps.setString(8,raw);ps.executeUpdate();}} private Map evolutionAprovada(long emp)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT * FROM evolution_instancias WHERE empresa_id=? AND status='aprovada' ORDER BY aprovado_em DESC,id DESC LIMIT 1")){ps.setLong(1,emp);try(ResultSet rs=ps.executeQuery()){return rs.next()?map(rs):null;}}} private Map one(Connection c,String sql,Object...params)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){bind(ps,params);try(ResultSet rs=ps.executeQuery()){return rs.next()?map(rs):null;}}} private Map map(ResultSet rs)throws SQLException{Mapm=new LinkedHashMap<>();ResultSetMetaData md=rs.getMetaData();for(int i=1;i<=md.getColumnCount();i++)m.put(md.getColumnLabel(i),rs.getObject(i));return m;} @@ -265,7 +272,9 @@ public final class ExternalBridgeHandler implements HttpHandler { private String str(Object v){return v==null?"":String.valueOf(v);} private String emptyToNull(String v){return v==null||v.isBlank()?null:v;} private String firstNonBlank(String... values){for(String v:values)if(v!=null&&!v.isBlank())return v;return "";} - private String extract(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return null;int colon=raw.indexOf(':',p);if(colon<0)return null;int q=raw.indexOf('"',colon+1);int e=q<0?-1:raw.indexOf('"',q+1);if(q<0||e<0)return null;return raw.substring(q+1,e).split("@")[0];} + private String extract(String raw,String key){String value=extractValue(raw,key);return value==null?null:value.split("@")[0];} + private String extractValue(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return null;int colon=raw.indexOf(':',p);if(colon<0)return null;int q=raw.indexOf('"',colon+1);int e=q<0?-1:raw.indexOf('"',q+1);if(q<0||e<0)return null;return raw.substring(q+1,e);} + private boolean isBlank(String value){return value==null||value.isBlank();} private boolean isFromMe(String raw){String marker="\"fromMe\"";int p=raw.indexOf(marker);if(p<0)return false;int colon=raw.indexOf(':',p);if(colon<0)return false;String tail=raw.substring(colon+1,Math.min(raw.length(),colon+12)).trim();return tail.startsWith("true");} private String resumo(String value){if(value==null)return "";return value.length()>120?value.substring(0,120):value;} private record Destinatario(String tipo,long id,String nome,String telefone){} diff --git a/src/main/resources/migrations/master/021_integracao_rastreamento_evolution.sql b/src/main/resources/migrations/master/021_integracao_rastreamento_evolution.sql new file mode 100644 index 0000000..316b2f5 --- /dev/null +++ b/src/main/resources/migrations/master/021_integracao_rastreamento_evolution.sql @@ -0,0 +1,24 @@ +ALTER TABLE integracao_mensagens ADD COLUMN IF NOT EXISTS evolution_message_id VARCHAR(160); +ALTER TABLE integracao_mensagens ADD COLUMN IF NOT EXISTS evolution_remote_jid VARCHAR(180); +ALTER TABLE integracao_mensagens ADD COLUMN IF NOT EXISTS evolution_remote_jid_alt VARCHAR(180); + +CREATE INDEX IF NOT EXISTS idx_integracao_mensagens_evolution_message ON integracao_mensagens(evolution_message_id); +CREATE INDEX IF NOT EXISTS idx_integracao_mensagens_evolution_remote ON integracao_mensagens(evolution_remote_jid, criado_em DESC); +CREATE INDEX IF NOT EXISTS idx_integracao_mensagens_evolution_remote_alt ON integracao_mensagens(evolution_remote_jid_alt, criado_em DESC); + +CREATE TABLE IF NOT EXISTS integracao_webhook_eventos ( + id BIGSERIAL PRIMARY KEY, + origem VARCHAR(40) NOT NULL DEFAULT 'evolution', + telefone VARCHAR(30), + remote_jid VARCHAR(180), + remote_jid_alt VARCHAR(180), + message_id VARCHAR(180), + texto TEXT, + integracao_mensagem_id BIGINT REFERENCES integracao_mensagens(id), + status VARCHAR(40) NOT NULL, + payload TEXT NOT NULL, + criado_em TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_integracao_webhook_eventos_criado ON integracao_webhook_eventos(criado_em DESC); +CREATE INDEX IF NOT EXISTS idx_integracao_webhook_eventos_telefone ON integracao_webhook_eventos(telefone, criado_em DESC);