Pular para o conteúdo
Development

Adicionando uma integração de newsletter com o Listmonk ao blog-manager

Por Victor Da Luz
railsrubynewsletterdev-logblog-manager

Esse item ficou no backlog com uma nota dizendo “depois, quando a newsletter tiver um ritmo”, não era urgente, mas decidi construir o encanamento agora em vez de esperar, já que a própria interface administrativa do Listmonk funciona bem enquanto isso e eu queria a integração pronta antes de realmente precisar dela.

O objetivo: enviar a newsletter semanal do blog a partir do blog-manager em vez da própria tela de composição do Listmonk, seguindo o mesmo padrão de “cliente de API nativo” que usei para Medium e Dev.to no começo do ano.

O que eu construí

Um model NewsletterSend, um serviço Listmonk::Client, dois jobs, e uma pequena interface, escolher posts, criar uma campanha rascunho no Listmonk, depois enviar.

A decisão interessante foi não reaproveitar o padrão de colunas em Post usado para Medium/Dev.to. Aqueles são 1:1, um post, uma cópia remota, um enum de status na linha de Post. Um envio de newsletter é um lote: vários posts, uma campanha, enviada para uma lista. Forçar isso em colunas de Post teria significado rastrear “em qual newsletter esse post foi incluído” como algum tipo de gambiarra has-many do lado errado do relacionamento. Então virou um model próprio, ligado aos posts por uma tabela de junção simples:

class NewsletterSend < ApplicationRecord
  has_many :newsletter_send_posts, dependent: :destroy
  has_many :posts, through: :newsletter_send_posts

  enum :status, { draft: 0, sending: 1, sent: 2, failed: 3 }
end

A própria API do Listmonk acabou usando um fluxo de envio com a mesma forma do Dev.to: criar um rascunho primeiro (POST /api/campaigns), depois deixá-lo ativo com uma segunda chamada (PUT /api/campaigns/:id/status {"status":"running"}). Mesma forma de duas etapas, “preparar e depois confirmar”, fornecedor diferente. A autenticação também é um esquema próprio, Authorization: token user:token, não Bearer, não HTTP Basic Auth de verdade, mesmo parecendo que deveria ser. Existe uma issue aberta no repositório do Listmonk sobre o Basic Auth não funcionar direito, então fui direto com o esquema token documentado em vez de descobrir isso do jeito difícil.

O que me surpreendeu

Fui fazer um smoke test do cliente contra a instância real do Listmonk, já em produção (estava parada meio inacabada no homelab, esperando um relay de SMTP). O primeiro curl para o hostname público voltou connection refused, o que parecia que o serviço estava fora do ar. Não estava, o hostname público resolve para um IP preso atrás de um túnel que não é alcançável de um cliente comum na rede local, mas o endereço direto do contêiner na rede local (192.168.20.133:9000) respondeu na hora. Anotei isso na base de conhecimento do homelab já que vai pegar quem quer que esbarre nisso depois: um serviço inalcançável pelo hostname público não significa que o serviço está fora do ar, pode só significar que você não está no túnel atrás do qual esse hostname está preso.

Com isso resolvido, rodei o cliente contra a API real com um token qualquer e recebi exatamente o que esperava: 403 {"message":"invalid API credentials"}, corretamente levantado como um AuthError. Bom o suficiente para confiar na forma da requisição sem precisar de um token de verdade ainda.

A outra surpresa veio do Brakeman, não do Listmonk. Meu primeiro rascunho da lista de “posts incluídos” iterava newsletter_send.posts.each e linkava o título de cada post para sua URL ao vivo, exatamente o mesmo padrão já usado em outros lugares desse app para links de post. O Brakeman marcou isso mesmo assim como um alerta fraco de XSS. Acontece que ele consegue rastrear um atributo de model até um padrão conhecido como seguro quando vem de um registro carregado direto por um controller, mas não quando está iterando por uma associação has_many :through, ele simplesmente desiste e chama isso de um “Unresolved Model,” o que tira o atributo da lista de padrões seguros. O fix foi mecânico assim que entendi: pré-computar um array simples de hashes antes do loop em vez de chamar .live_url dentro dele. Mesma saída, mas o Brakeman não consegue rastrear a proveniência de um atributo através de um hash Ruby simples, então o alerta desaparece. Esse repositório roda o Brakeman com --exit-on-warn no CI, então esse alerta sozinho teria travado tudo.

O que vem depois

O deploy do Listmonk ainda está esperando o próprio relay de SMTP e os registros de DNS, então consegui verificar a criação de campanha rascunho contra a API real, mas não um envio de verdade entregue, essa é a única parte disso que estou entregando sem verificar, e deixei isso claro no PR em vez de fingir o contrário. Quando isso se resolver, o passo restante é só uma pessoa: criar um usuário/token de API na própria interface administrativa do Listmonk e um ID de lista de assinantes, colocar os dois na página de configurações do blog-manager, e o fluxo de envio fica no ar.

Leitura relacionada

Você também pode achar útil

Proton

Proton Pass

Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais