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

  • SkyDSL2+ : photos du matériel KaSat

    Quelques photos prises lors de la réception de notre kit KaSat. Commençons par la tête de réception, soigneusement emballée : La visserie, classée par ordre, plus simple pour le montage : Le câble coaxial de 25 m reliant la tête au modem : Le modem satellite : Le package SkyDSL2+ vous est livré par UPS. La première facture nous a été adressée en début de semaine, reprenant l’abonnement mensuel, les frais d’ouverture, et, cette même semaine, UPS nous livrait 3…

  • AirOS V5.2.1-Beta2 disponible

    Ce jour sort la version AirOS V5.2.1-Beta 2, la suite des évolutions logicielles du firmware destiné aux matériels Airmax d’Ubiquiti. Les modifications sont : v5.2.1-Beta2 Changelog: – Show static routes in Main page, when operating in Bridge mode – Station list is empty on IE browser – Sometimes after AP reboot or FW upgrade, station cannot associate to AP anymore (security ON) V5.2.1-Beta Changelog: – Multicast enhancement on AP problems for multicast traffic (RSTP/OSPF doesn’t pass wireless link) – Multicast…

  • Freemobile loue les smartphones

    Nouvelle idée de Freemobile, vous faire louer vos smartphones sur 24 mois. Seulement voilà, le loup est bien caché : ne surtout pas l’abîmer, sinon le prix public de vente en vigueur au moment de la commande vous sera facturé. Une assurance sera donc plus que recommandée, un vrai profit pour celui qui va la délivrer  donc, en prenant soin de bien lire les petites lignes qui vont forcément exclure tel ou tel cas des conditions. en cas de vol,  «…

  • SkyDSL : bricolo et bricolette font un sondage

    Notre résiliation de l’offre SkyDSL 2 MBps approchant, le délicieux service client de l’opérateur, après nous avoir fait part des regrets de nous voir partir (…), nous a proposé un sondage. Direction la page du sondage donc : On frôle de nouveau l’absurde : le forfait : classic, premium, professionnel, autant de noms qui ne correspondent à aucune des offres 2, 6 ou 10 Mbps proposées par SkyDSL. Comment répondre alors ?? combien de Gb ont été téléchargés ? Euh……

  • Sat2Way : abonnement internet par satellite SANS location

    Si vous avez déjà le matériel Tooway pour le satellite Kasat, vous pouvez désormais prendre un abonnement chez Sat2way, sans que les différents matériels vous soient facturés. A ce jour, les offres débutent à 17 € mensuels. Seules conditions à remplir : être le propriétaire du matériel, et être libre de tout engagement vis-à-vis d’un autre opérateur. Attention à bien vous en assurer, notamment vis à vis de SkyDSL qui semble avoir la réputation de verrouiller l’adresse MAC de votre modem Tooway dans…

  • Tooway : station de contrôle KaSat

    Les plus curieux d’entre vous pourront se faire une idée des installations d’une station de contrôle au sol du satellite KaSat en téléchargeant le ZIP disponible sur le site de l’opérateur, Eutelsat. C’est cette station qui contrôle par exemple l’offre SkyDLS2+ que nous distribuons. De bien belles photos qui relèguent votre installation de réception TV à bien peu de chose 😉  

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.