105 lines
3.2 KiB
Ruby
105 lines
3.2 KiB
Ruby
# 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
|
|
|
|
# Grupos de que o número conectado participa. O id do grupo é opaco, então a
|
|
# tela precisa desta lista para o ADM escolher pelo nome.
|
|
def grupos
|
|
get('/grupos')
|
|
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
|