Como escrever um BOT para o BlueSky que posta imagens

Sim, eu sei, a rede "Threads" que é a bambambam da vez, parece até que a BlueSky já ficou obsoleta, antes mesmo de sair da fase de convites.

Mas enquanto as coisas não se estabilizam, sigo no Twitter, Mastodon, BlueSky e Threads (nem vou falar de Koo aqui porque esse é um blog de família).

Mas o Threads ainda não tem vários recursos que as outras redes têm. Como eu já tinha o meu botzinho que posta tirinhas no Twitter e no Mastodon diariamente, queria que ele postasse também no BlueSky, daí eu fiz e estou escrevendo esse texto porque a documentação dele é bem limitada ainda. O BlueSky utiliza o AT Protocol, que é parecido com o esquema do Mastodon, mas tem suas diferenças.

Buscando na internet, é até fácil achar textos sobre como fazer um app postar textos no BlueSky. Primeiro você precisa criar a "App Password". É só ir em Settings < Apps passwords.

Print da configuração do BlueSky

Clicar em "Add app password".

Print da configuração do BlueSky

Escolher um nome para o app.

Print da configuração do BlueSky

Copiar a senha gerada (e guardar num lugar seguro).

Print da configuração do BlueSky

Feito isso, podemos gerar o nosso bot.

Como eu disse, era fácil encontrar um código padrão. Então, encontrei esse mesmo código em mais de um lugar, às vezes com acesso ao OpenAI, às vezes sem (é Node.js):

import * as dotenv from 'dotenv'
import blue from '@atproto/api';
import ai from 'openai'

dotenv.config()
const { BskyAgent } = blue;
const { Configuration, OpenAIApi } = ai;
const configuration = new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(configuration);
const generateFunnyCatQuote = async () => {
    // get the post from the openAI API
    const completion = await openai.createChatCompletion({
        model: "gpt-3.5-turbo",
        messages: [{role: "user", content: "Generate a funny cat quote Tweet, omit the hashtags."}],
    });
    // terrible way to deal with the fact that ChatGPT sometimes puts the posts in ""
    const postText = completion.data.choices[0].message.content.slice(1, completion.data.choices[0].message.content.lastIndexOf('"') )
    console.log("Post:"+postText);
    
    //now that we have the post, lets post it to bluesky
    const {RichText} = blue;
    const agent = new BskyAgent({ service: 'https://bsky.social/' })
    await agent.login({identifier: process.env.BLUESKY_BOT_EMAIL, password: process.env.BLUESKY_BOT_PASSWORD})
    const rt = new RichText({text: postText })
    const postRecord = {
        $type: 'app.bsky.feed.post',
        text: rt.text,
        facets: rt.facets,
        createdAt: new Date().toISOString()
    }
    await agent.post(postRecord)
};

generateFunnyCatQuote();

Fica de brinde a parte do "OpenAI", mas eu não preciso disso. Então mexi no código para ficar mais "enxugado".

const blue = require("@atproto/api");
require("dotenv").config();

const { BskyAgent } = blue;

const BLUESKY_BOT_USERNAME = process.env.BLUESKY_APP_USERNAME;
const BLUESKY_BOT_PASSWORD = process.env.BLUESKY_APP_PASSWORD;

const postBlueSky = async (tweet) => {
  const { RichText } = blue;
  const agent = new BskyAgent({ service: "https://bsky.social/" });
  await agent.login({
    identifier: BLUESKY_BOT_USERNAME,
    password: BLUESKY_BOT_PASSWORD,
  });

  const rt = new RichText({ text: tweet });
  const postRecord = {
    $type: "app.bsky.feed.post",
    text: rt.text,
    facets: rt.facets,
    createdAt: new Date().toISOString(),
  };
  await agent.post(postRecord);
};

Podem ver que deixei o parâmetro do texto com o nome "tweet". No Twitter chama "tweet", no Mastodon chama "toot", não sei como é o nome oficial no BlueSky, mas deixei "tweet" mesmo...

O código funciona que é uma beleza. Daí o problema era subir a imagem.

A ideia quando se faz um bot desses, nas diferentes redes, é você subir a imagem, pegar o identificador gerado pela imagem enviada e embutir esse identificador no seu post. Porém, qual é o formato de envio? Quais são os parâmetros do protocolo?

Encontrei um código em chinês e outro numa língua que não reconheci, que se aproximavam do que eu queria, mas não era o que eu precisava, porque usavam outros recursos e eu queria algo mais clean.

Voltei na documentação, lá tem as definições léxicas da API. Fui na parte dos "embed", tem o léxico do "external", que é para quando se coloca um link com miniatura (os famosos "cards"), tem o "images", que é o que eu precisava, depois tem os "records" (que ignorei).

Então, a ideia era subir a imagem em BLOB. O meu botzinho já lia a imagem das tirinhas, gerava o blob e passava para base64, para o Twitter. No Bluesky não precisa passar para base64.

Depois de subir o BLOB, a ideia era montar a estrutura do "embed" para enviar junto com o "tweet". Vão ver que a função final aceita posts com ou sem imagens, fazendo um teste se adiciona o embed ou não.

const blue = require("@atproto/api");
require("dotenv").config();

const { BskyAgent } = blue;

const BLUESKY_BOT_USERNAME = process.env.BLUESKY_APP_USERNAME;
const BLUESKY_BOT_PASSWORD = process.env.BLUESKY_APP_PASSWORD;

const postBlueSky = async (tweet, image, alt) => {
  const { RichText } = blue;
  const agent = new BskyAgent({ service: "https://bsky.social/" });
  await agent.login({
    identifier: BLUESKY_BOT_USERNAME,
    password: BLUESKY_BOT_PASSWORD,
  });

  let embed = null;
  if (image) {
    const uploadedImage = await agent.uploadBlob(image, {
      encoding: 'image/png',
    } );
    if (!uploadedImage) throw new Error("Failed to upload blob");

    embed = {
      $type: 'app.bsky.embed.images',
      images: [
        {
          image: {  
            $type: 'blob',
            ref: {
              $link: uploadedImage.data.blob.ref.toString(),
            },
            mimeType: uploadedImage.data.blob.mimeType,
            size: uploadedImage.data.blob.size,
          },
          alt: alt,
        }
      ]
    };
  }

  const rt = new RichText({ text: tweet });
  const postRecord = {
    $type: "app.bsky.feed.post",
    text: rt.text,
    facets: rt.facets,
    createdAt: new Date().toISOString(),
  };
  if (embed) postRecord.embed = embed;
  await agent.post(postRecord);
};

E daí, meu botzinho agora faz posts no twitter, mastodon e bluesky, sendo chamados num cronjob.

↑ Voltar ao Topo