AutoBlueSky : Publication RSS vers Bluesky

Ce projet permet de publier automatiquement les nouveaux articles d’un flux RSS vers le réseau social Bluesky.

Le script est conçu pour Python 3. Il récupère les articles, extrait intelligemment l’image de couverture et le résumé (via les balises OpenGraph), et publie le tout sous forme de « Card » riche.

Prérequis

  • Un serveur Linux (VPS, Raspberry Pi, etc.)
  • Python 3 installé (python3 --version)
  • Un compte Bluesky actif

Étape 1 : récupérer le mot de passe d’application

Pour utiliser ce script, vous ne devez jamais utiliser votre mot de passe de connexion habituel. Vous devez générer un mot de passe spécifique pour ce script.

  1. Connectez-vous à votre compte Bluesky.
  2. Rendez-vous dans les Paramètres > Mots de passe d’application (ou cliquez directement sur ce lien : https://bsky.app/settings/app-passwords).
  3. Cliquez sur le bouton « Ajouter un mot de passe d’application ».
  4. Donnez-lui un nom (ex: Script RSS Blog).
  5. Copiez la chaîne de caractères qui s’affiche (format : xxxx-xxxx-xxxx-xxxx). C’est ce code que vous utiliserez dans la configuration à l’étape 3.

Étape 2 : installation

1. Préparation de l’environnement Python 3

Il est recommandé d’isoler les dépendances du script dans un environnement virtuel.

# Créez le dossier du projet
mkdir ~/autobluesky
cd ~/autobluesky

# Créez l'environnement virtuel Python 3
python3 -m venv venv

# Activez l'environnement
source venv/bin/activate

2. Installation des librairies

Installez les bibliothèques nécessaires à l’exécution du script :

pip3 install feedparser python-dotenv atproto

3. Fichier de configuration

Créez un fichier pour stocker vos identifiants en sécurité.

nano autobluesky.conf

Collez le contenu suivant en remplaçant les valeurs par les votre :

BLUESKY_HANDLE=votre-pseudo.bsky.social
BLUESKY_PASSWORD=xxxx-xxxx-xxxx-xxxx

(Utilisez le mot de passe en xxxx-... généré à l’Étape 1).


Étape 3 : le script (autobluesky.py)

Créez le fichier du script :

nano autobluesky.py

Collez l’intégralité du code ci-dessous.

⚠️ Important : Pensez à modifier les lignes RSS_FEED_URL, env_path et STATE_FILE au début du script pour qu’elles correspondent à votre propre dossier utilisateur (remplacez /home/maison/ par votre chemin).

#!/usr/bin/env python3

#
# Publication automatique sur BlueSky des articles
# du Blog
#
# A ajouter sur une tache crontab
# Configuration des jetons dans rss_social_env
#


import feedparser
import json
import os
import urllib.request
import re
from atproto import Client, client_utils, models
from dotenv import load_dotenv

# --- Configuration ---
env_path = os.path.expanduser('/home/maison/autobluesky.conf')
load_dotenv(env_path)

RSS_FEED_URL = "https://monblog.fr/feed"
STATE_FILE = '/home/maison/rss_state.json'
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

def load_state():
    if os.path.exists(STATE_FILE):
        try:
            with open(STATE_FILE, 'r') as f: return json.load(f)
        except: return []
    return []

def save_state(published_urls):
    with open(STATE_FILE, 'w') as f: json.dump(published_urls, f, indent=4)

def get_detailed_info(url):
    """ Extrait l'image et le résumé en fouillant le HTML """
    try:
        req = urllib.request.Request(url, headers=HEADERS)
        with urllib.request.urlopen(req, timeout=10) as response:
            html = response.read().decode('utf-8', errors='ignore')
            
            # Extraction Image
            img_match = re.search(r'property="og:image" content="(.*?)"', html)
            if not img_match:
                all_imgs = re.findall(r'<img [^>]*src="([^"]*(?:jpg|jpeg|png))"', html)
                img_url = next((i for i in all_imgs if not any(x in i.lower() for x in ['logo', 'avatar', 'icon'])), None)
            else:
                img_url = img_match.group(1)

            # Extraction Résumé
            desc_match = re.search(r'property="og:description" content="(.*?)"', html)
            if not desc_match:
                paragraphs = re.findall(r'<p>(.*?)</p>', html, re.DOTALL)
                real_p = [re.sub('<[^<]+?>', '', p).strip() for p in paragraphs if len(p) > 50]
                summary = real_p[0] if real_p else "Découvrez notre nouvel article sur MonBlog !"
            else:
                summary = desc_match.group(1)
            
            return img_url, summary
    except:
        return None, "Découvrez notre nouvel article sur MonBlog !"

def main():
    client = Client()
    handle = os.getenv("BLUESKY_HANDLE")
    password = os.getenv("BLUESKY_PASSWORD")
    
    if not handle or not password:
        return

    try:
        client.login(handle, password)
    except Exception as e:
        print(f"❌ Erreur connexion : {e}")
        return

    published_urls = load_state()
    
    try:
        req = urllib.request.Request(RSS_FEED_URL, headers=HEADERS)
        with urllib.request.urlopen(req, timeout=15) as response:
            feed = feedparser.parse(response.read())
    except Exception:
        return

    # On ne traite que les 10 derniers billets du flux
    new_entries = [e for e in feed.entries[:10] if e.link not in published_urls]

    if not new_entries:
        print("💤 Rien de nouveau à publier.")
        return

    # Publication automatique (du plus ancien au plus récent)
    for entry in reversed(new_entries):
        print(f"🚀 Publication auto : {entry.title}")
        img_url, summary = get_detailed_info(entry.link)
        
        try:
            embed_external = None
            if img_url:
                try:
                    img_req = urllib.request.Request(img_url, headers=HEADERS)
                    img_data = urllib.request.urlopen(img_req).read()
                    if len(img_data) < 1000000:
                        upload = client.upload_blob(img_data)
                        embed_external = models.AppBskyEmbedExternal.Main(
                            external=models.AppBskyEmbedExternal.External(
                                title=entry.title,
                                description=summary[:200],
                                uri=entry.link,
                                thumb=upload.blob
                            )
                        )
                except: pass

            tb = client_utils.TextBuilder()
            tb.text(f"📰 {entry.title}\n\n🔗 ")
            tb.link("Lire l'article sur MonBlog", entry.link)
            
            client.send_post(text=tb, embed=embed_external)
            
            # On enregistre immédiatement pour éviter les doublons en cas de plantage
            published_urls.append(entry.link)
            save_state(published_urls)
            print("✅ OK")

        except Exception as e:
            print(f"❌ Erreur sur {entry.title}: {e}")

if __name__ == "__main__":
    main()

Rendez le script exécutable :

chmod +x autobluesky.py

Étape 4 : automatisation

Pour lancer le script automatiquement (par exemple toutes les heures), utilisez le planificateur de tâches Cron.

  1. Ouvrez l’éditeur cron :
crontab -e
  1. Ajoutez la ligne suivante à la fin du fichier :(Adaptez les chemins selon votre nom d’utilisateur)
# Lance le script toutes les heures à la minute 00
0 * * * * /home/maison/autobluesky/venv/bin/python3 /home/maison/autobluesky/autobluesky.py >> /tmp/autobluesky.log 2>&1

Note : On appelle directement l’exécutable python3 situé dans le dossier venv. Cela permet au script d’accéder aux librairies installées sans avoir besoin d’activer manuellement l’environnement.

Script de test : test_bluesky.py

Ce script est minimaliste : il tente de se connecter et d’afficher le nom de ton profil pour confirmer que le Mot de passe d’application est valide.

⚠️ Important : Pensez à modifier les lignes pour qu’elles correspondent à votre propre dossier utilisateur (remplacez /home/maison/ par votre chemin).

#!/usr/bin/env python3
import os
from atproto import Client
from dotenv import load_dotenv

# Configuration du chemin
env_path = os.path.expanduser('/home/maison/autobluesky.conf')
load_dotenv(env_path)

def test_connection():
    handle = os.getenv("BLUESKY_HANDLE")
    password = os.getenv("BLUESKY_PASSWORD")
    
    if not handle or not password:
        print("❌ Erreur : Identifiants introuvables dans /home/maison/autobluesky.conf")
        return

    client = Client()
    try:
        print(f"⏳ Tentative de connexion pour {handle}...")
        profile = client.login(handle, password)
        print(f"✅ Connexion réussie !")
        print(f"👤 Nom affiché sur le profil : {profile.display_name}")
    except Exception as e:
        print(f"❌ Échec de la connexion : {e}")
        print("\nVérifiez votre App Password ici : https://bsky.app/settings/app-passwords")

if __name__ == "__main__":
    test_connection()

Publications similaires

  • Somfy : domotisez vos volets avec un ESP32

    Somfy, le célèbre fabriquant de volets roulants, connu pour ses modules au prix fort et son protocole propriétaire peut désormais s’intégrer dans votre domotique en utilisant un simple ESP32. C’est ce que propose ESPSomfy RTS, pour vos commandes RTS uniquement (pas IO) sur https://github.com/rstrouse/ESPSomfy-RTS Rien de très sorcier mais un programme conçu de façon élégante, à relier à un simple module radio CC101 : C’est précisément de module qui, piloté par votre ESP32, fournira les trames radio adaptées à vos…

  • #Freemobile : bienvenue dans un monde sans débits

    1er Septembre 2016, nous y voici, c’est aujourd’hui que Freemobile débute le bridage des débits pour ses nombreux abonnés hélas contraints d’utiliser une antenne Orange, officiellement du moins car personnellement sur une antenne Orange je n’ai jamais constaté des débits pharaoniques, et je ne suis visiblement pas le seul. Nous avons donc imaginé une affiche publicitaire ironiquement adaptée à ce changement radical : Sauf erreur, c’est une première dans le monde de la téléphonie mobile : ponctionner chaque mois le…

  • Du Wimax chez D-Link ?

    Le constructeur D-Link vient d’annoncer son association avec Samsung pour la vente de routeurs Wimax. L’idée est d’associer les téléviseurs Samsung et les routeurs D-Link pour leur offrir un accès internet. D’ici à penser qu’une gamme Wimax va enfin faire son apparition dans nos habitations… Source : Présence-PC

  • Le RCEA refuse le Wimax

    Clermain, mardi soir, le collectif Rassemblement des Clerminois pour un Environnement d’Avenir (NDR : oh le joli mot…), membre de l’association Robin des Toits, a manifesté dans le village et devant sa mairie du village pour refuser l’installation de l’antenne WiMax. Créé le 21 novembre, le RCEA fait circuler une pétition ayant déjà recueilli 100 signatures sur les 168 inscrits sur la liste électorale. Le plus amusant dans cette histoire, exception faite d’une communication systématiquement articulée autour de la peur, réside…

  • Sat2way : nouvelles offres (màj)

    Le 1er Février, Sat2way lancera de nouvelles offres sur le satellite internet KaSat, avec 20 Mbps de débit en descente, 6 Mbps en montée, toujours via une parabole de 77 cm de diamètre. Pour commencer, une offre Tooway ABSOLUTE, 74,90 € mensuels, réservée aux 20 000 premiers abonnés européens, offrant un accès totalement illimité. Pour les offres L et XL, l’accès illimité sera mis en place durant la nuit, permettant de gérer au mieux la réception de fichiers volumineux :…

  • 151 000 hotspots FreeWiFi

    Pas loin de 151 000 hotspots FreeWifi auraient été recensés par les seuls iPhone ou mobiles Androids depuis le lancement de ce service par l’opérateur alternatif Free. Rappelons que ce service ne concerne que les Freebox V5 en zone dégroupée, ce qui a toute son importance : ne comptez pas le trouver dans votre village perdu en pleine campagne, ce ne sera pas le cas hélas. Précision amusante : comme pour le précédent article sur FreeWiFi, les commentaires sur ce…

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.