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

  • Arrivée des produits 3G

    Les produits 3G arrivent dès aujourd’hui sur la boutique en ligne d’Infracom. Comme toujours, nous ne souhaitons pas proposer ce que d’autres font déjà, mais plutôt viser le mouton à 5 pattes en proposant de la fabrication sur mesure par exemple. Pour le moment, vous retrouverez des répéteurs GSM, des répéteurs GSM/3G, une antenne 3G à gain et des cordons pour connecter une antenne externe sur la plupart des modems 3G Huawei. Nous sommes toutefois en mesure de réaliser des…

  • Nordnet : 20 Mbps sur Astra 2F

    Lancé en Novembre 2012, ASTRA 2F n’avait pas encore bénéficié d’une remise à niveau des offres des FAI. Voilà chose faite avec l’offre JET de Nordnet, proposant une descente à 20 Mbps et une montée à 2 Mbps, pour un volume de 10 Go/mois et un tarif de 29,90 €/mois. Si le volume ne suffit pas, ajoutez 15 € et vous bénéficierez de 40 Go/mois. Notez pour finir que le volume n’est pas décompté de 23h à 7h du matin, à vous donc…

  • Distribuez le Wimax en WiFi

    Sprint a tout compris pour séduire ses clients : distribuer leur connexion Wimax en utilisant un point d’accès WiFi ! C’est ce que permet le Sprint Overdrive, un tout petit module chargé d’interfacer les deux mondes pour $350 ( 260 €) ou 50$ (38 €) avec un contrat de deux ans. Voici la publicité de Sprint, in english : A noter que Sprint propose aussi le MiFi, un hotspot personnel :

  • 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…

  • Et ça tourne, ça tourne…

    Un festival, c »est une véritable ruche, sans cesse en mouvement, chacun disposant d’une tâche bien précise, d’une fonction indispensable au fonctionnement du TOUT, ce TOUT qui fait la réussite de l’évènement. Il suffit de se poser et d’observer pour se rendre compte de cette agitation. Vous croiserez ainsi un tracteur baladant un fourgon frigorifique, le service de nettoyage des ordures (et il y en a un maximum sur site et backstage !!), la vidange des toilettes, les différents stands de nourriture venant…

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.