Bluesky.bot

Agendar publicações

Um bot não precisa de estar sempre a correr. A configuração fiável mais simples é um script que corre de poucos em poucos minutos, publica o que está na hora e termina.

Publicar só o que está na hora

Dê ao script um calendário e um intervalo de execução fixo. Cada execução publica os itens cuja hora chegou desde a execução anterior: nada é publicado duas vezes e nada falha, desde que as execuções cheguem a horas.

scheduled.ts
// scheduled.ts: execute a cada 15 minutos (cron, GitHub Actions...).
// Cada execução publica o que ficou na hora desde a anterior,
// por isso nada é publicado duas vezes.
import { AtpAgent } from "@atproto/api";

const RUN_EVERY_MS = 15 * 60 * 1000;

const SCHEDULE = [
  { at: "2026-11-02T08:00:00Z", text: "Good morning! Today's forecast is up." },
  { at: "2026-11-02T18:00:00Z", text: "Evening update: clear skies tonight." },
];

const now = Date.now();
const due = SCHEDULE.filter((post) => {
  const at = Date.parse(post.at);
  return at <= now && at > now - RUN_EVERY_MS;
});

if (due.length > 0) {
  const agent = new AtpAgent({ service: "https://bsky.social" });
  await agent.login({
    identifier: process.env.BLUESKY_HANDLE ?? "",
    password: process.env.BLUESKY_APP_PASSWORD ?? "",
  });
  for (const post of due) {
    await agent.post({ text: post.text, createdAt: new Date().toISOString() });
    console.log(`Posted: ${post.text}`);
  }
} else {
  console.log("Nothing due.");
}

O mesmo em Python:

scheduled.py
# scheduled.py: execute a cada 15 minutos (cron, GitHub Actions...).
# Cada execução publica o que ficou na hora desde a anterior,
# por isso nada é publicado duas vezes.
import os
from datetime import datetime, timedelta, timezone

from atproto import Client

RUN_EVERY = timedelta(minutes=15)

SCHEDULE = [
    ("2026-11-02T08:00:00+00:00", "Good morning! Today's forecast is up."),
    ("2026-11-02T18:00:00+00:00", "Evening update: clear skies tonight."),
]

now = datetime.now(timezone.utc)
due = [
    text
    for at, text in SCHEDULE
    if now - RUN_EVERY < datetime.fromisoformat(at) <= now
]

if due:
    client = Client()
    client.login(os.environ["BLUESKY_HANDLE"], os.environ["BLUESKY_APP_PASSWORD"])
    for text in due:
        client.send_post(text)
        print(f"Posted: {text}")
else:
    print("Nothing due.")

Para um calendário que muda, leia-o de um ficheiro ou de uma base de dados em vez do código, e registe o que foi publicado, para que uma execução atrasada ou repetida não possa publicar duas vezes.

Executá-lo com cron

Num servidor que controla, execute crontab -e e acrescente uma linha (a cada 15 minutos):

bash
*/15 * * * * cd /home/bot/my-bot && BLUESKY_HANDLE=weather.bluesky.bot BLUESKY_APP_PASSWORD=$(cat ~/.bluesky-app-password) npx tsx scheduled.ts >> bot.log 2>&1

Executá-lo com GitHub Actions

No repositório do seu bot, acrescente .github/workflows/post.yml e guarde as credenciais como segredos do repositório (Settings → Secrets and variables → Actions):

yaml
name: Post
on:
  schedule:
    - cron: "*/15 * * * *"
  workflow_dispatch:
jobs:
  post:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx tsx scheduled.ts
        env:
          BLUESKY_HANDLE: ${{ secrets.BLUESKY_HANDLE }}
          BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}

O GitHub executa os workflows agendados sem garantias: uma execução pode começar tarde ou, com muita carga, ser saltada. Mantenha no script um intervalo igual ao do calendário, e não conte com o minuto exato.

Respeitar os limites

  • Publique a um ritmo humano. Um bot que inunda os feeds é silenciado, denunciado ou limitado.
  • Entre uma vez por execução, não uma vez por publicação: os inícios de sessão têm o seu próprio limite, mais apertado.
  • Leia os limites de utilização do Bluesky antes de pôr a correr um bot muito ativo.