Track Evolution identifiers for bridge replies

This commit is contained in:
Tiago
2026-09-29 17:30:03 -03:00
parent 3e9ee4db53
commit 21bbc8d385
3 changed files with 63 additions and 28 deletions
@@ -142,15 +142,17 @@ public final class EvolutionApiClient {
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("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")
@@ -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<String,Object> 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<String,Object> 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<Destinatario> dests(Connection c, String sql, String tipo, Object... params) throws SQLException {List<Destinatario> 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<Destinatario> 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<String,Object> buscarEnvioResposta(Connection c,String numero,String remoteJid,String remoteJidAlt)throws SQLException{Map<String,Object> 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<String,Object> 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<String,Object> 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<String,Object> map(ResultSet rs)throws SQLException{Map<String,Object>m=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){}
@@ -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);