Implantação da geração de mensagem de forma livre e mudança na engine de mensagem

This commit is contained in:
2026-08-24 17:32:16 -03:00
parent 94bbe7556f
commit 88f19dd8f5
77 changed files with 4650 additions and 25 deletions

View File

@@ -0,0 +1,98 @@
# app/services/notificacao/cliente_whatsapp.rb
#
# Cliente da ponte Baileys (container `whatsapp`). Substitui o Twilio como canal
# padrão de WhatsApp.
#
# ⚠️ Canal NÃO OFICIAL: a ponte fala o protocolo do WhatsApp Web por engenharia
# reversa. É gratuito e ilimitado, mas está fora dos Termos do WhatsApp e a
# Meta pode banir o número. Por isso: chip dedicado, e o intervalo entre
# mensagens é respeitado por quem chama (Notificacao::Despachante), não aqui.
#
# TIMEOUT em tudo: sem ele, uma ponte pendurada trava o worker Puma até o proxy
# cortar — o mesmo motivo que levou o ClienteTwilio a configurar timeout.
module Notificacao
class ClienteWhatsapp
TIMEOUT_ABERTURA = 5
TIMEOUT_LEITURA = 20
Resposta = Struct.new(:ok, :dados, :erro, keyword_init: true) do
def ok? = ok
end
def self.padrao
config = ConfiguracaoNotificacao.instancia
new(url: config.baileys_url_efetiva, token: config.baileys_token_efetivo)
end
def initialize(url:, token:)
@url = url.to_s.strip.chomp('/')
@token = token.to_s
end
def configurado?
@url.present? && @token.present?
end
# { conectado:, numero:, qr:, desde:, ultimo_erro: }
def status
get('/status')
end
def enviar(para:, texto:)
post('/enviar', para: para, texto: texto)
end
def desconectar
post('/logout')
end
private
def get(caminho)
requisitar(Net::HTTP::Get, caminho)
end
def post(caminho, **corpo)
requisitar(Net::HTTP::Post, caminho, corpo)
end
def requisitar(classe, caminho, corpo = nil)
return Resposta.new(ok: false, erro: 'Ponte do WhatsApp não configurada.') unless configurado?
uri = URI.join("#{@url}/", caminho.delete_prefix('/'))
req = classe.new(uri)
req['Authorization'] = "Bearer #{@token}"
req['Content-Type'] = 'application/json'
req.body = corpo.to_json if corpo
resposta = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: uri.scheme == 'https',
open_timeout: TIMEOUT_ABERTURA,
read_timeout: TIMEOUT_LEITURA) { |http| http.request(req) }
dados = parse(resposta.body)
if resposta.is_a?(Net::HTTPSuccess)
Resposta.new(ok: true, dados: dados)
else
Resposta.new(ok: false, dados: dados, erro: mensagem_de_erro(resposta, dados))
end
rescue StandardError => e
# A ponte fora do ar não pode virar 500 na tela nem quebrar um fechamento.
Rails.logger.error("[ClienteWhatsapp] #{e.class}: #{e.message}")
Resposta.new(ok: false, erro: "#{e.class}: #{e.message}")
end
def parse(corpo)
JSON.parse(corpo.to_s)
rescue JSON::ParserError
{}
end
def mensagem_de_erro(resposta, dados)
return 'Token da ponte do WhatsApp inválido.' if resposta.code == '401'
return 'WhatsApp desconectado — leia o QR code na tela.' if resposta.code == '503'
dados['erro'].presence || "HTTP #{resposta.code}"
end
end
end