Implantação da geração de mensagem de forma livre e mudança na engine de mensagem
This commit is contained in:
159
app/services/notificacao/renderizador.rb
Normal file
159
app/services/notificacao/renderizador.rb
Normal file
@@ -0,0 +1,159 @@
|
||||
# app/services/notificacao/renderizador.rb
|
||||
#
|
||||
# Blocos + dados => mensagem pronta. Duas saídas do MESMO template:
|
||||
#
|
||||
# #texto → WhatsApp (texto puro, com o *negrito* do WhatsApp)
|
||||
# #html → e-mail (tabelas com estilo inline, que é o que cliente de e-mail
|
||||
# renderiza de forma previsível)
|
||||
#
|
||||
# ⚠️ TODO texto vindo do editor e TODO valor de variável passa por escape no
|
||||
# caminho HTML. O corpo é digitado numa tela e viraria injeção de HTML no
|
||||
# e-mail — e o preview usa este mesmo renderizador, então um XSS aqui
|
||||
# atingiria primeiro o próprio admin.
|
||||
#
|
||||
# O preview da tela chama exatamente estes métodos: uma segunda implementação em
|
||||
# JavaScript inevitavelmente divergiria do que é enviado de verdade.
|
||||
module Notificacao
|
||||
class Renderizador
|
||||
# Cores fixas: cliente de e-mail não lê CSS externo nem variável de tema.
|
||||
LARANJA = '#f97316'.freeze
|
||||
ESCURO = '#111111'.freeze
|
||||
CINZA = '#666666'.freeze
|
||||
|
||||
def initialize(blocos, dados = {})
|
||||
@blocos = Array(blocos).select { |b| b.is_a?(Hash) && Blocos.valido?(b['tipo']) }
|
||||
@dados = (dados || {}).transform_keys(&:to_s)
|
||||
end
|
||||
|
||||
# ── WhatsApp ────────────────────────────────────────────────
|
||||
def texto
|
||||
@blocos.filter_map { |bloco| texto_do(bloco) }.join("\n\n").strip
|
||||
end
|
||||
|
||||
# ── E-mail ──────────────────────────────────────────────────
|
||||
def html
|
||||
corpo = @blocos.filter_map { |bloco| html_do(bloco) }.join("\n")
|
||||
<<~HTML
|
||||
<div style="font-family:Arial,Helvetica,sans-serif;font-size:15px;color:#{ESCURO};line-height:1.6;max-width:600px;">
|
||||
#{corpo}
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
# Substitui {{variavel}} pelos dados. Variável sem valor vira string vazia —
|
||||
# deixar "{{valor}}" cru numa mensagem enviada é pior do que deixar o espaço.
|
||||
def interpolar(texto)
|
||||
texto.to_s.gsub(/\{\{\s*(\w+)\s*\}\}/) { @dados[Regexp.last_match(1)].to_s }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def escapar(valor) = ERB::Util.html_escape(interpolar(valor))
|
||||
|
||||
def texto_do(bloco)
|
||||
case bloco['tipo']
|
||||
when 'cabecalho'
|
||||
[negrito(bloco['titulo']), interpolar(bloco['subtitulo'])].reject(&:blank?).join("\n").presence
|
||||
when 'texto', 'rodape'
|
||||
interpolar(bloco['texto']).presence
|
||||
when 'aviso'
|
||||
conteudo = interpolar(bloco['texto'])
|
||||
conteudo.present? ? "⚠️ #{conteudo}" : nil
|
||||
when 'tabela'
|
||||
linhas = Array(bloco['linhas']).filter_map do |linha|
|
||||
next unless linha.is_a?(Hash)
|
||||
rotulo = interpolar(linha['rotulo'])
|
||||
valor = interpolar(linha['valor'])
|
||||
next if rotulo.blank? && valor.blank?
|
||||
"• #{rotulo}: #{valor}".strip
|
||||
end
|
||||
linhas.any? ? linhas.join("\n") : nil
|
||||
when 'botao'
|
||||
url = interpolar(bloco['url'])
|
||||
return nil if url.blank?
|
||||
[interpolar(bloco['rotulo']).presence, url].compact.join(': ')
|
||||
when 'divisor'
|
||||
'——————————'
|
||||
end
|
||||
end
|
||||
|
||||
def negrito(valor)
|
||||
conteudo = interpolar(valor)
|
||||
conteudo.present? ? "*#{conteudo}*" : ''
|
||||
end
|
||||
|
||||
def html_do(bloco)
|
||||
case bloco['tipo']
|
||||
when 'cabecalho' then html_cabecalho(bloco)
|
||||
when 'texto' then html_paragrafo(bloco['texto'])
|
||||
when 'aviso' then html_aviso(bloco)
|
||||
when 'tabela' then html_tabela(bloco)
|
||||
when 'botao' then html_botao(bloco)
|
||||
when 'divisor' then %(<hr style="border:none;border-top:1px solid #e5e5e5;margin:20px 0;">)
|
||||
when 'rodape' then html_rodape(bloco)
|
||||
end
|
||||
end
|
||||
|
||||
def html_cabecalho(bloco)
|
||||
titulo = escapar(bloco['titulo'])
|
||||
sub = escapar(bloco['subtitulo'])
|
||||
return nil if titulo.blank? && sub.blank?
|
||||
|
||||
partes = []
|
||||
partes << %(<h1 style="margin:0 0 4px;font-size:20px;color:#{ESCURO};">#{titulo}</h1>) if titulo.present?
|
||||
partes << %(<p style="margin:0 0 16px;font-size:14px;color:#{CINZA};">#{sub}</p>) if sub.present?
|
||||
partes.join("\n")
|
||||
end
|
||||
|
||||
# `simple_format` não serve aqui: ele produz HTML sem os estilos inline que o
|
||||
# cliente de e-mail precisa. As quebras de linha viram <br>, já escapadas.
|
||||
def html_paragrafo(valor)
|
||||
conteudo = escapar(valor)
|
||||
return nil if conteudo.blank?
|
||||
|
||||
%(<p style="margin:0 0 14px;">#{conteudo.gsub("\n", '<br>')}</p>)
|
||||
end
|
||||
|
||||
def html_aviso(bloco)
|
||||
conteudo = escapar(bloco['texto'])
|
||||
return nil if conteudo.blank?
|
||||
|
||||
%(<div style="margin:0 0 16px;padding:12px 14px;background:#fff8e1;border-left:4px solid #f0ad4e;) +
|
||||
%(font-size:14px;color:#5c4400;">#{conteudo.gsub("\n", '<br>')}</div>)
|
||||
end
|
||||
|
||||
def html_tabela(bloco)
|
||||
linhas = Array(bloco['linhas']).filter_map do |linha|
|
||||
next unless linha.is_a?(Hash)
|
||||
rotulo = escapar(linha['rotulo'])
|
||||
valor = escapar(linha['valor'])
|
||||
next if rotulo.blank? && valor.blank?
|
||||
|
||||
%(<tr><td style="padding:8px 12px;border-bottom:1px solid #eee;color:#{CINZA};">#{rotulo}</td>) +
|
||||
%(<td style="padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold;">#{valor}</td></tr>)
|
||||
end
|
||||
return nil if linhas.empty?
|
||||
|
||||
%(<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;margin:0 0 16px;font-size:14px;">) +
|
||||
linhas.join + '</table>'
|
||||
end
|
||||
|
||||
def html_botao(bloco)
|
||||
url = interpolar(bloco['url']).to_s.strip
|
||||
# Só http(s): `javascript:` num href montado na tela seria clique armado.
|
||||
return nil unless url.match?(%r{\Ahttps?://}i)
|
||||
|
||||
rotulo = escapar(bloco['rotulo']).presence || 'Abrir'
|
||||
%(<p style="margin:0 0 18px;"><a href="#{ERB::Util.html_escape(url)}" ) +
|
||||
%(style="display:inline-block;padding:11px 20px;background:#{LARANJA};color:#000;) +
|
||||
%(text-decoration:none;border-radius:8px;font-weight:bold;font-size:14px;">#{rotulo}</a></p>)
|
||||
end
|
||||
|
||||
def html_rodape(bloco)
|
||||
conteudo = escapar(bloco['texto'])
|
||||
return nil if conteudo.blank?
|
||||
|
||||
%(<p style="margin:24px 0 0;font-size:12px;color:#999;">#{conteudo.gsub("\n", '<br>')}</p>)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user