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 : à 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 : à 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) :
*/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>&1Le 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) :
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.