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

  • Python 3 : jour ou nuit ?

    Pour vos projets domotique, il peut être sympa de venir détecter les changements entre jour et nuit, par exemple pour pouvoir allumer automatiquement les lampes intérieures, activer la lumière extérieure lorsque vous le demandez, etc. Voici donc un petit script en Python 3, sun.py, qui vous indiquera s’il fait jour, ou s’il fait nuit : #!/usr/bin/python3 import ephem import datetime import time somewhere = ephem.Observer() somewhere.lat = ‘47.411252’ somewhere.lon = ‘-2.169095’ somewhere.elevation = 112 # # Heure actuelle convertie en chiffres…

  • Wimax mobile : étonnant…

    Vous vous demandez encore quel intérêt peut avoir le WiMax mobile ? Regardez la photo prise depuis une voiture lors d’un test réalisé à Chicago lors de l’exposition « WiMax World » : Vous avez bien vu les chiffres ? 5289 kpbs en réception, 2804 kbps en émission…. Je me suis amusé à aller voir les offres d’Altitude Telecom pour comparer : 1 Mbps maxi dans l’offre pour les particuliers 4 Mbps pour les professionnels Je suis méchant avec Altitude Telecom ?…

  • Bize débat sur son antenne

    Décidément, les journalistes de Ladepeche.fr adorent le sujet du Wimax et les peurs que cette technologie irradiante génère, puisqu’ils se fendent d’un nouvel article : « L’association ADSL sans Wimax vient d’être créée. Son objectif : sensibiliser la population et sur le risque et les effets secondaires provoqués par ses faisceaux et mettre en débat sur son édification à Bize. Le collectif organise une réunion publique d’information à la salle des fêtes de Bize, le vendredi 22 octobre, à 20 h…

  • L’Inde, Wimax puissance 50 !

    L’Economic Times, en Inde, annonce que Tata Communications (TCL) dispose désormais de 50 000 abonnés à son service Wimax fixe. TCL utilise la bande 3.3 GHz pour opérer avec sa licence de fournisseur de service internet délivrée par le gouvernement Indien. D’autres opérateurs, tels Bharti Airtel, Aircel, Sify et Reliance Communications utilisent également cette gamme de fréquences. Environ 1400 stations de base ont été déployées par TCL, dans 140 villes. Cela représente le réseau Wimax le plus étendu au monde….

  • Freemobile : 50 Go de marketing sans risque

    Décidément, l’opérateur bricolo, Freemobile, est le champion de la communication : nouveau forfait, 50 Go, mais à condition de capter son réseau 4G, ce qui ne concerne, pour le moment, qu’une infime partie de la population française, et une encore plus infirme partie du territoire métropolitain, autant dire ne pas y compter, sauf à vous trouver proche des trop rares antennes 4G. Exception faite donc du coté technique de l’offre un peu bricolée, c’est encore une fois le génie marketing…

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.