Pular para o conteúdo
Development

Reconciliando cross-posts do Medium que o blog-manager nunca tinha registrado

Por Victor Da Luz
railsrubysyndicationdev-logblog-manager

Uso o blog-manager para acompanhar o status de syndication de cada post que faço cross-post do vdaluz.com para o Dev.to e o Medium via Postiz. O problema: alguns dos meus cross-posts no Medium aconteceram antes do blog-manager existir, e mais alguns foram agendados manualmente direto no painel do Medium, fora de qualquer fluxo conduzido pelo Postiz. O blog-manager não tinha registro de nenhum dos dois grupos, então mostrava esses posts como “não sincronizados” mesmo estando lá, publicados no Medium.

O pedido era simples no papel: verificar os artigos ativos do vdaluz.com e marcar os que já estavam no Medium. O que eu de fato encontrei foi uma sequência de momentos “espera, não era isso que eu tinha assumido” que tornaram a funcionalidade mais interessante do que a issue sugeria.

O que eu construí

O blog-manager já rastreia o estado de publicação por canal numa coluna JSON em Post, postiz_publish, indexada pelo id de integração do Postiz, com entradas como { "state" => "PUBLISHED", "provider" => "medium", "release_url" => "..." }. Um acessor postiz_medium varre esse hash procurando a entrada onde provider == "medium", sem se importar com qual é a chave. Esse último detalhe acabou importando muito.

O Medium aposentou sua API em janeiro, então o único caminho de integração que sobrou é um sidecar de automação de navegador rodando no homelab, que conduz o fluxo “Import a story” do Medium. Isso é via de mão única: só publica, nada de consultar. Mas o Medium ainda serve um feed RSS público por perfil, sem precisar de autenticação, então construí o Medium::FeedReconciler: buscar o perfil do Medium conectado no endpoint /integrations do Postiz, derivar a URL do feed (medium.com/feed/@handle), buscá-la, e comparar os itens com os posts locais por título normalizado exato.

Como o reconciler não tem um id real de integração do Postiz para um post que descobre dessa forma, dei ao Post#backfill_medium! uma chave sintética e uma regra de precedência: PUBLISHED vence SCHEDULED que vence DRAFT, e o processo só faz upgrade, nunca downgrade. Isso me permitiu também tratar um segundo caso: posts que já estavam na fila privada “Scheduled” do Medium, que eu não consigo descobrir automaticamente de jeito nenhum (mais sobre isso abaixo), mas posso preencher retroativamente assim que souber deles, e depois ver serem promovidos para PUBLISHED automaticamente assim que a próxima execução do reconciler pegar eles entrando no ar.

Decisões que tomei e por quê

Decidi não construir uma interface de “possível correspondência, confirma?” com correspondência difusa para títulos que não batem exatamente. Meu primeiro instinto foi que cross-posts antigos poderiam ter títulos editados, então eu precisaria de alguma tolerância. Acabou sendo um problema menor do que eu esperava (veja abaixo), e os dois casos de correspondência de título que precisei tratar são estreitos o bastante para que um override manual numa rake task seja melhor do que um fluxo de confirmação inteiro, por enquanto. Três linhas parecidas valem mais do que uma abstração prematura.

Também mantive o reconciler automatizado e o backfill histórico como duas rake tasks separadas (medium:reconcile versus medium:seed_scheduled / medium:seed_published) em vez de tentar unificá-las. Elas têm níveis de confiança genuinamente diferentes: uma roda contra um feed ao vivo e verificável; a outra roda contra um dump de dados manual e único, que não consigo reverificar automaticamente. Misturar as duas teria escondido essa diferença.

O que me surpreendeu

O feed RSS só retorna as 10 histórias publicadas mais recentes. Sem paginação, sem parâmetro de limite, nada. Tenho 78 posts publicados no Medium, e o feed simplesmente… para em 10. Isso não está documentado em lugar nenhum óbvio; descobri buscando o feed real e contando os itens. Isso significa que o job automatizado vai capturar corretamente cada post novo dali para frente, mas nunca vai conseguir percorrer para trás pelo meu próprio histórico de publicações. Para isso eu precisei da lista real, o que significou exportar manualmente o painel Published do próprio Medium.

A primeira tentativa dessa exportação não funcionou. Salvei a página como HTML e obtive um instantâneo genérico da home, deslogado, sem nenhuma menção ao meu usuário em lugar nenhum, porque o painel do Medium é renderizado no lado do cliente depois do login, e um salvamento simples de página não espera isso acontecer. O que funcionou foi copiar o texto já renderizado diretamente, do mesmo jeito que eu já tinha feito para a aba Scheduled.

Com títulos reais para comparar, rodei a lógica de correspondência exata contra os 78 e obtive 76 acertos automáticos. Das duas falhas, uma foi uma edição de título genuína (“my website” virou “this site” no post atual, confirmado lendo o arquivo de verdade) e a outra foi um post legitimamente diferente e sem relação, que por acaso tem um nome muito parecido. Nenhuma das duas precisou de correspondência difusa; precisaram de um humano olhando para dois casos específicos, exatamente a escala em que um override manual se encaixa.

Também encontrei código morto enquanto estava por ali: uma rake task do Dev.to chamando uma classe que não existe em lugar nenhum da aplicação, claramente sobrando de antes de a publicação no Dev.to migrar para o Postiz. Não mexi nisso, registrei como um follow-up à parte em vez de deixar o escopo desta issue crescer.

Próximos passos

76 dos 78 posts históricos e todos os 8 agendados estão preenchidos retroativamente. Daqui para frente, o job recorrente que roda a cada hora mantém o quadro atualizado sozinho. A pergunta em aberto é se vale a pena estender o mesmo sidecar de automação de navegador que já publica no Medium para que ele também raspe diretamente as listas autenticadas Published/Scheduled, fechando a lacuna que o RSS não alcança sem outra exportação manual da próxima vez. Registrado como uma decisão para mais tarde, não como algo para construir agora.

Leitura relacionada

Development

Um toggle por blog, e quando não "corrigir" um bug

Um booleano atravessando quatro camadas, e três passadas de revisão independentes concordando com um achado que eu deliberadamente não corrigi - porque o cache é o mecanismo de dedup.

Ler

Você também pode achar útil

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
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