Bluesky.bot

Programmer des posts

Un bot n’a pas besoin de tourner en permanence. La solution fiable la plus simple est un script qui se lance toutes les quelques minutes, publie ce qui est dû, puis s’arrête.

Publier seulement ce qui est dû

Donnez au script un calendrier et un intervalle d’exécution fixe. Chaque exécution publie les éléments dont l’heure est passée depuis l’exécution précédente : rien n’est publié deux fois et rien n’est oublié, tant que les exécutions restent à l’heure.

scheduled.ts
// scheduled.ts : à lancer toutes les 15 minutes (cron, GitHub Actions…).
// Chaque exécution publie ce qui est devenu dû depuis la précédente :
// rien n’est publié deux fois.
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.");
}

La même chose en Python :

scheduled.py
# scheduled.py : à lancer toutes les 15 minutes (cron, GitHub Actions…).
# Chaque exécution publie ce qui est devenu dû depuis la précédente :
# rien n’est publié deux fois.
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.")

Pour un calendrier qui change, lisez-le dans un fichier ou une base de données plutôt que dans le code, et enregistrez ce qui a été publié, pour qu’une exécution en retard ou répétée ne puisse pas publier deux fois.

Le lancer avec cron

Sur un serveur que vous contrôlez, lancez crontab -e et ajoutez une ligne (toutes les 15 minutes) :

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

Le lancer avec GitHub Actions

Dans le dépôt de votre bot, ajoutez .github/workflows/post.yml, et stockez les identifiants comme secrets du dépôt (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 }}

GitHub lance les workflows programmés au mieux : une exécution peut démarrer en retard ou, sous forte charge, être sautée. Gardez dans le script un intervalle égal à celui du calendrier, et ne comptez pas sur la minute exacte.

Rester dans les limites

  • Publiez à un rythme humain. Un bot qui inonde les fils se fait masquer, signaler ou limiter.
  • Connectez-vous une fois par exécution, pas une fois par post : les connexions ont leur propre limite, plus stricte.
  • Lisez les limites de débit de Bluesky avant de lancer un bot très actif.