Executando verificação de segurança...
4

Como criei uma newsletter sobre sneakers automática usando Cloud Functions e Inteligência artificial

Criar uma newsletter automática que consome feeds RSS e resume o conteúdo sobre sneakers foi um projeto interessante que combinou diversas tecnologias, incluindo Cloud Functions, RSS parsing, e processamento de linguagem natural com OpenAI. Aqui está uma visão técnica detalhada de como fiz isso. Caso você queria se inscrever para receber o conteúdo é só acessar https://sneakerpedia.com.br

Estrutura do Projeto

  • Cloud Functions para agendamento: Utilizei Cloud Functions do Firebase para agendar a execução diária da geração da newsletter.
  • Consumo de Feeds RSS: Feeds RSS são consumidos e processados para extrair as notícias mais recentes, usei o feed ao invés de usar o webscrapping, ficou mais simples e eu ainda consigo economizar nos tokens :)
  • Resumo e Tradução de Conteúdo: Utilizamos a API do OpenAI para resumir e traduzir os títulos das notícias.
  • Geração da Newsletter: O conteúdo é formatado em Markdown para criar a newsletter.
  • Publicação no Instagram: A primeira notícia é publicada automaticamente no Instagram.

Um pouco do código fonte

Este arquivo contém a função agendada que inicia o processo de criação da newsletter diariamente à meia-noite.

    const { onSchedule } = require("firebase-functions/v2/scheduler");
    const createBroadcast = require("./lib/createBroadcast");
    const config = require("./config/config");

    exports.helloWorld = onSchedule("every day 00:00", () =>
      createBroadcast(config.feeds)
    );

Este arquivo contém a lógica principal para consumir feeds RSS, resumir o conteúdo, formatar a newsletter e publicá-la.

const fs = require("fs");
const path = require("path");
const Parser = require("rss-parser");
const {
  translateTitle,
  summarizeNews,
  generateIntroduction,
  createNewsletterTitle,
  checkNewsContent,
} = require("./ai");
const { publisInstagram } = require("./publishInstagram");

const parser = new Parser();
const imageRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/i;
const placeholderImageUrl = "https://via.placeholder.com/150";

const formatCategoryNews = async (news) => {
  try {
    const imageMarkdown = `\n![Imagem](${news.image})\n`;
    return `
## ${news.title}
${imageMarkdown}
${await checkNewsContent(news.summary)}
    `;
  } catch (error) {
    console.error("Erro ao formatar categoria de notícias:", error);
    return null;
  }
};

const getNewsFromRSSFeeds = async (feeds) => {
  let allNews = [];
  for (const feedOptions of feeds) {
    try {
      const feed = await parser.parseURL(feedOptions.feedUrl);
      const newsFromFeedPromises = feed.items.slice(0, 3).map(async (item) => {
        const imageMatch = item[feedOptions.contentIn].match(imageRegex);
        const imageUrl = imageMatch ? imageMatch[1] : placeholderImageUrl;
        return {
          title: await translateTitle(item.title),
          link: item.link,
          content: item[feedOptions.contentIn],
          image: imageUrl,
          summary: await summarizeNews(item[feedOptions.contentIn]),
          name: feedOptions.name,
          alreadySent: false,
        };
      });
      const newsFromFeed = await Promise.all(newsFromFeedPromises);
      allNews = allNews.concat(newsFromFeed);
    } catch (error) {
      console.error(`Erro ao buscar notícias do feed ${feedOptions.name}:`, error);
    }
  }
  return allNews;
};

const createNewsletter = async (feeds) => {
  const allNews = await getNewsFromRSSFeeds(feeds);
  try {
    const titles = allNews.map((item) => item.title);
    const newsletterTitle = await createNewsletterTitle(titles);
    const introduction = await generateIntroduction(allNews);
    let formattedNewsletter = `# ${newsletterTitle}\n\n${introduction}\n`;
    for (const item of allNews) {
      const formattedCategory = await formatCategoryNews(item);
      formattedNewsletter += formattedCategory;
    }
    return {
      formattedNewsletter,
      newsletterTitle,
      allNews,
    };
  } catch (error) {
    console.error("Erro ao formatar newsletter:", error);
    return null;
  }
};

const createBroadcast = async (feeds) => {
  try {
    const newsletter = await createNewsletter(feeds);
    if (!newsletter) {
      throw new Error("Newsletter não pôde ser criada");
    }
    const filePath = path.join(__dirname, "newsletter.md");
    fs.writeFileSync(filePath, newsletter.formattedNewsletter);
    console.log(`Newsletter criada com sucesso: ${filePath}`);
    console.log(`Publicando no instagram a primeira noticia`);
    await publisInstagram(
      newsletter.allNews[0].title,
      newsletter.allNews[0].image,
      newsletter.allNews[0].summary
    );
  } catch (error) {
    console.error("Erro ao criar broadcast:", error);
  }
};

module.exports = createBroadcast;

Optei por deixar apenas a criação de um markdown, para usar no behiive, plataforma bem bacana de newsletter que conheci esses dias. Mas poderiam ter outros outputs e até mesmo uma criação direta com a API, infelizmente o behiive não tem o recurso para criação de posts :/

Carregando publicação patrocinada...