About “vibe coding” #LLM #AI #coding

Un compteur pour déterminer quand Trump va être dégagé (ou pas)

Documentation du Script N8N

Description

Ce script JavaScript est conçu pour N8N afin de calculer et de publier quotidiennement des décomptes avant des événements politiques majeurs aux États-Unis, à savoir :

  • Les Midterms (élections de mi-mandat).
  • L'élection présidentielle.
  • Le jour de l’investiture.

Il génère également une barre de progression graphique pour chaque événement sous forme de texte.

Fonctionnalités

1. Définition des dates clés

Le script définit les dates des événements cibles :

  • Midterms : 3 novembre 2026.
  • Élection présidentielle : 7 novembre 2028.
  • Jour de l’investiture : 20 janvier 2029.

2. Calcul des jours restants

Le script calcule :

  • Le nombre total de jours entre aujourd’hui et chaque événement.
  • Le pourcentage de progression en fonction des jours écoulés.

3. Génération de barres de progression

Une fonction génère une barre de progression graphique, composée de blocs pleins () et de blocs vides (), représentant visuellement l’avancée jusqu’à l’événement.

4. Génération et publication du message

Le script produit un message comprenant :

  • Le décompte des jours restants pour chaque événement.
  • Les barres de progression associées.

Exemple de Résultat

Voici un exemple du message généré :

There are 646 days until the Midterms, 1381 days until the next Presidential Election, and 1490 days until the next Inauguration Day.
Midterms Progress: █▒▒▒▒▒▒▒▒▒ 9%
Presidential Election Progress: █▒▒▒▒▒▒▒▒▒ 4% 
Until Inauguration: ▒▒▒▒▒▒▒▒▒▒ 0%

Code

// JavaScript code for N8N to calculate and post daily countdowns with a graphical loading bar

// Define the reference start date (Trump's 2025 inauguration)
const startOfMandate = new Date('2025-01-20T00:00:00Z');

// Define the target dates
const midtermsDate = new Date('2026-11-03T00:00:00Z'); // Next USA Midterms
const presidentialElectionDate = new Date('2028-11-07T00:00:00Z'); // Next Presidential Election
const inaugurationDate = new Date('2029-01-20T00:00:00Z'); // Next Inauguration Day

// Get the current date
const currentDate = new Date();

// Function to calculate correct progress
function calculateProgress(targetDate) {
  const totalDays = Math.ceil((targetDate - startOfMandate) / (1000 * 60 * 60 * 24));
  const elapsedDays = Math.ceil((currentDate - startOfMandate) / (1000 * 60 * 60 * 24));
  return Math.min(100, Math.max(0, Math.floor((elapsedDays / totalDays) * 100)));
}

// Calculate days remaining
const daysUntilMidterms = Math.ceil((midtermsDate - currentDate) / (1000 * 60 * 60 * 24));
const daysUntilPresidentialElection = Math.ceil((presidentialElectionDate - currentDate) / (1000 * 60 * 60 * 24));
const daysUntilInauguration = Math.ceil((inaugurationDate - currentDate) / (1000 * 60 * 60 * 24));

// Calculate accurate progress percentages
const midtermsProgress = calculateProgress(midtermsDate);
const presidentialProgress = calculateProgress(presidentialElectionDate);
const inaugurationProgress = calculateProgress(inaugurationDate);

// Create a graphical loading bar function
function createLoadingBar(percentage) {
  const totalBars = 10; // Length of the loading bar
  const filledBars = Math.floor((percentage / 100) * totalBars);
  const emptyBars = totalBars - filledBars;
  return `${'█'.repeat(filledBars)}${'▒'.repeat(emptyBars)} ${percentage}%`;
}

// Generate the loading bars
const midtermsLoadingBar = createLoadingBar(midtermsProgress);
const presidentialLoadingBar = createLoadingBar(presidentialProgress);
const inaugurationLoadingBar = createLoadingBar(inaugurationProgress);

// Generate the message
const message = `There are ${daysUntilMidterms} days until the Midterms, ${daysUntilPresidentialElection} days until the next Presidential Election, and ${daysUntilInauguration} days until the next Inauguration Day.\n\n` +
  `Midterms Progress: \n${midtermsLoadingBar}\n\n` +
  `Presidential Election Progress: \n${presidentialLoadingBar}\n\n` +
  `Inauguration Progress: \n${inaugurationLoadingBar}`;

// Output the message
return [{
  json: {
    message,
  },
}];

Où il publie

Le script retourne le message sous forme d’un objet JSON, prêt à être utilisé dans un flux N8N pour une publication quotidienne via un nœud horaire configuré.

Configuration Recommandée

  • Fuseau horaire du serveur : CET (heure allemande).
  • Heure de publication : 14h00 CET, correspondant à 8h00 ET (heure de la côte Est des États-Unis).

Utilisation

  1. Intégrez le script dans un nœud de fonction JavaScript dans N8N.

  2. Ajoutez un nœud horaire configuré pour exécuter le flux quotidiennement.3. Reliez le nœud de fonction à un nœud de sortie ou à un service tiers pour publier le message (par exemple Bluesky.).

Résultat

Trumpwatch

Bluesky Won’t Save Us Like radiation, social media is invisible scientific effluence that leaves us both more knowledgeable and more ignorant of the causes of our own afflictions than ever

#bluesky #moderation #mastodon

Nice Map of GitHub

anvaka.github.io/map-of-gi…

#dataviz #github #coding

The link leads to a #dataviz of the github coding landscape&10;Shades of blue differentiate different coding languages and by zooming in, one can explore different github repositories.

Ça fait plusieurs semaines qu’au boulot je nage entre des Keycloak, des Tyk, des frontend #AngularJS et mine de rien ça avance, on va pouvoir décommissioner tout un bazar de serveurs qui a plusieurs années, hors support, non maintenu, bref une cata ambulante 🖖🏽

interesting for local development anchor.dev/blog/intr… #lclhost

Happy to be using many of these tools, both at home and work :)

Yay Github Multi Account Support 😎

Mon extension Chrome/Vivaldi/Edge pour poster sur Bluesky marche en mode basique !

Mon extension Chrome/Vivaldi/Edge pour poster sur Bluesky depuis n’importe quel tab sur lequel on est dans le navigateur fonctionne, en mode décoffrage brut : le titre et le lien sont postés, mais pas de carte intégrée et le lien n’est pas clickable, mais pour 2 ou 3h de dev ça me va, next corriger les bugs!!

c’est aussi la première fois de ma vie que je crée une extension pour chrome et c’est une chouette learning curve !

ce qui marche

  • login
  • click to share
  • text posts

ce qui ne marche pas

  • URL avec carte intégrée
  • se déconnecter via l’extension

JSON to RSS feed with N8N and optional HTML manipulation

I’m so thrilled to finally having done it right !

This workflow that you can copy and use on N8N does a few things

  • HTTP request a JSON feed to get last published items at the source (belgian AFSCA)
  • Using regex to pick image file from the description field
  • Clean up of the Description field from any html tags
  • Serve a new RSS feed (Afsca does not have one)
  • Use the RSS feed to Publish anywhere

Testing your feed

  • Disable the workflow
  • Disable the Feed node (left webhook)
  • Enable Manual Execution
  • Run the Workflow to inspect it and pin data if you need to test it

To enable the servicing of the RSS feed

  • Disable the Manual Execution node
  • Enable the Feed node (left webhook)
  • Enable the workflow
  • Visit your production RSS URL provided by the Feed node

Use view-source:https://URL of your RSS feed to easily inspect the content of your rss feed. You can use Feedbro to test your RSS feed locally

Copy into N8N

  • Select the entire content of this block code below and paste it inside N8N
  • you will get the node and everything like in the image above.
  • the Function node showcased below does not need copying, it’s already included in the workflow.
{
  "meta": {
    "instanceId": "58fd5c3ff393ef21f618201de491e9a03a72661d4848a2ad337fc80dc260a4d9"
  },
  "nodes": [
    {
      "parameters": {},
      "id": "c7da8a26-baa3-47e5-a3c5-9f886d67dce4",
      "name": "When clicking \"Execute Workflow\"",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        460,
        760
      ],
      "disabled": true
    },
    {
      "parameters": {
        "fieldToSplitOut": "items",
        "options": {}
      },
      "id": "042e6e0d-11ff-46e9-b49e-f440f44d6fa2",
      "name": "Split out lists",
      "type": "n8n-nodes-base.itemLists",
      "position": [
        1120,
        600
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "path": "afsca.rss",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "bb82d17d-e134-40be-8f85-898600577ff5",
      "name": "Feed",
      "type": "n8n-nodes-base.webhook",
      "position": [
        460,
        460
      ],
      "webhookId": "63f12265-8387-4bb7-bef0-d7eb93e49e11",
      "typeVersion": 1
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json[\"data\"] }}",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/rss+xml"
              }
            ]
          }
        }
      },
      "id": "e6857661-3b79-4c8b-a73d-c67ef4aebd9d",
      "name": "Serve feed",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2040,
        600
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "url": "https://www.inoreader.com/stream/user/1005072895/tag/Afsca/view/json",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "4361bdc9-2795-4ac5-99eb-efc4db72a64c",
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        820,
        600
      ]
    },
    {
      "parameters": {
        "functionCode": "const escapeHTML = str => {\n    if (!str) return \"\";\n    return str.replace(/[&<>'\"]/g, \n        tag => ({\n            '&': '&',\n            '<': '<',\n            '>': '>',\n            \"'\": ''',\n            '\"': '"'\n        }[tag])\n    );\n};\n\nconst unescapeHTML = str => {\n    if (!str) return \"\";\n    return str.replace(/(<|>|"|'|&)/g, \n        tag => ({\n            '<': '<',\n            '>': '>',\n            '"': '\"',\n            ''': \"'\",\n            '&': '&'\n        }[tag])\n    );\n};\n\nlet feedItems = [];\nfor (item of items) {\n    feedItems.push(`<item>\n        <title><![CDATA[${unescapeHTML(item.json.Title)}]]></title>\n        <guid isPermaLink=\"false\">${item.json.guid}</guid>\n        <media:content url=\"${item.json.Image}\" type=\"image/jpeg\" />\n        <link>${item.json.Link}</link>\n        <pubDate>${DateTime.fromISO(item.json.Date).toRFC2822()}</pubDate>\n        <description><![CDATA[${unescapeHTML(item.json.Description || \"\")}]]></description>\n    </item>`);\n}\n\nconst feedTitle = \"RappelConso\";  // Set this to your desired feed title\nconst feedDescription = \"Rappel Conso monitoring.\";  // Set this to your desired feed description\nconst feedLink = \"https://rmendes.net\";  // Set this to your main RSS page or main website URL\n\nreturn [{\n    data: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:media=\"http://search.yahoo.com/mrss/\" version=\"2.0\">\n    <channel>\n        <title><![CDATA[${feedTitle}]]></title>\n        <link>${feedLink}</link>\n        <description><![CDATA[${feedDescription}]]></description>\n        <pubDate>${DateTime.fromISO(item.json.Date).toRFC2822()}</pubDate>\n        ${feedItems.join('\\n')}\n    </channel>\n</rss>`\n}];\n\n"
      },
      "id": "986ea46f-4206-431e-92a9-199072492648",
      "name": "Define feed items1",
      "type": "n8n-nodes-base.function",
      "position": [
        1840,
        600
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "ImageURL",
              "value": "={{$json[\"content_html\"].match(/src=\"([^\"]+)\"/) ? $json[\"content_html\"].match(/src=\"([^\"]+)\"/)[1] : \"\"}}\n"
            },
            {
              "name": "CleanDesc",
              "value": "={{$json[\"content_html\"].replace(/<\\/?[^>]+(>|$)/g, \" \").trim().replace(/\\s\\s+/g, ' ').substring(0,200)}}\n"
            }
          ]
        },
        "options": {}
      },
      "id": "b1f7e9b0-3270-4a44-bfdf-5fa7519e9d6b",
      "name": "Fetch IMG + CleanDesc",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [
        1360,
        600
      ]
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "string": [
            {
              "name": "Title",
              "value": "={{ $json.title }}"
            },
            {
              "name": "Link",
              "value": "={{ $json.url }}"
            },
            {
              "name": "Date",
              "value": "={{ $json.date_published }}"
            },
            {
              "name": "guid",
              "value": "={{ $json.id }}"
            },
            {
              "name": "Description",
              "value": "={{ $json.CleanDesc }}"
            },
            {
              "name": "Image",
              "value": "={{ $json.ImageURL }}"
            }
          ]
        },
        "options": {}
      },
      "id": "5cdbd79f-6f5b-42ee-8f3d-b8d1de320949",
      "name": "Set Everything",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [
        1620,
        600
      ]
    }
  ],
  "connections": {
    "When clicking \"Execute Workflow\"": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split out lists": {
      "main": [
        [
          {
            "node": "Fetch IMG + CleanDesc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Feed": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Split out lists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Define feed items1": {
      "main": [
        [
          {
            "node": "Serve feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch IMG + CleanDesc": {
      "main": [
        [
          {
            "node": "Set Everything",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Everything": {
      "main": [
        [
          {
            "node": "Define feed items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Javascript function node used in this workflow

  • I have escapeHTML and unEscapeHTML for each use case, this is completely optional and pretty much there for me to have both use cases at hand when I work with feeds.
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
const escapeHTML = str => {
    if (!str) return "";
    return str.replace(/[&<>'"]/g, 
        tag => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            "'": '&#39;',
            '"': '&quot;'
        }[tag])
    );
};

const unescapeHTML = str => {
    if (!str) return "";
    return str.replace(/(&lt;|&gt;|&quot;|&#39;|&amp;)/g, 
        tag => ({
            '&lt;': '<',
            '&gt;': '>',
            '&quot;': '"',
            '&#39;': "'",
            '&amp;': '&'
        }[tag])
    );
};

let feedItems = [];
for (item of items) {
    feedItems.push(`<item>
        <title><![CDATA[${unescapeHTML(item.json.Title)}]]></title>
        <guid isPermaLink="false">${item.json.guid}</guid>
        <media:content url="${item.json.Image}" type="image/jpeg" />
        <link>${item.json.Link}</link>
        <pubDate>${DateTime.fromISO(item.json.Date).toRFC2822()}</pubDate>
        <description><![CDATA[${unescapeHTML(item.json.Description || "")}]]></description>
    </item>`);
}

const feedTitle = "RappelConso";  // Set this to your desired feed title
const feedDescription = "Rappel Conso monitoring.";  // Set this to your desired feed description
const feedLink = "https://rmendes.net";  // Set this to your main RSS page or main website URL

return [{
    data: `<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        <title><![CDATA[${feedTitle}]]></title>
        <link>${feedLink}</link>
        <description><![CDATA[${feedDescription}]]></description>
        <pubDate>${DateTime.fromISO(item.json.Date).toRFC2822()}</pubDate>
        ${feedItems.join('\n')}
    </channel>
</rss>`
}];

Bluesky, comment utiliser un newsbot pour générer des Customs Feeds thématiques ?

L’idée avec ce tuto, c’est de démontrer une mise en place d’utilisation des customs feeds sur Bluesky basé sur un News Bot qui va générer le contenu, ce qui va nous permettre de segmenter l’activité du newsbot en différents customs feeds auxquels les utilisateurs vont pouvoir s’abonner.

  • D’abord, j’ai été récupérer les Flux RSS de chez LeMonde.fr
  • Ensuite, j’ai mis en place un agrégateur de flux afin de rassembler les différents flux d’info par dossier (actu, culture, planète etc..)
  • Ensuite, j’ai mis en place le newsbot en tant que tel en suivant mon tuto

12 flux de veilles sur un même compte bluesky

Vue FreshRSS des catégories d’info LeMonde.fr

Il y a donc 12 processus de veille RSS qui tournent, un par catégorie maitresse sur le site LeMonde

Vue des 12 conteneurs qui font tourner le bot sur bluesky

Comment sont construits les custom feeds ?

J’avais besoin d’une ancre stable sur lequel me baser pour chaque catégorie et je ne pouvais pas prévoir tous les mots utilisés dans un titre ou la description pour segmenter les articles en différent flux thématique, du coup, en éditant mon fichier config.json, je peux segmenter avant même la publication, et ce, de manière stable :

config.json pour Actu

{
  "string": "Actu: $title",
  "publishEmbed": true,
  "languages": ["fr"],
  "truncate": true,
  "runInterval": 60,
  "dateField": ""
}

config.json pour Culture

{
  "string": "Culture: $title",
  "publishEmbed": true,
  "languages": ["fr"],
  "truncate": true,
  "runInterval": 60,
  "dateField": ""
}

Etc.. etc..ce qui me permet de prendre le dossier/catégorie culture, actu, sports, france etc… et d’avoir une segmentation simple sans devoir passer par des tas de requête regex qui ne donneraient pas une segmentation aussi simple et efficace.

Résultat de la veille sur Bluesky

Flux d’information LeMonde sur bluesky

Skyfeed custom feed builder

Vue de la mise en place d’un custom feed avec skyfeed.app

Vue de la construction d’un custom feed sur skyfeed

Listes des custom feeds sur @lfm.bsky.social

Vue du compte sur bluesky

Résultats :

Vue Skyfeed des différents flux thématique

le problème des doublons (résolu)

Pour régler les problèmes de doublons, c’est-à-dire, un article qui apparaît dans 2 ou 3 flux, j’ai déjà viré les flux à la Une, vu qu’ils reprennent le contenu des catégories, ensuite grâce à Inoreader, je vire les doublons d’une même catégorie en faisant un tri sur les articles qui ont le même titre, mais qui sont publiées à plusieurs endroits et enfin, on utilise le flux de sortir du dossier (culture, actu, sports etc..) comme input d’entrée du bot qui veille à l’arrivée de nouvelles publications et qui s’en charge de les publier.

Comment déployer un news bot RSS sur Bluesky?

pré-requis

  • un compte bluesky dédié
  • un “app password”
  • un flux RSS source de contenu à publier
  • bsky.rss
  • la commande git installée
  • Docker et Docker-Compose installé

Pour l’exercice, on va déployer un newsbot https://disclose.ngo/ C’est un média d’investigation.

Setup

Pas besoin d’un gros serveurs, il suffit d’avoir un ordinateur, évidemment si vous le débranché, le bot s’éteint aussi, donc, c’est plus pratique de mettre ça en ligne, sur un petit vps, même un serveur à 5€ par mois fera tout à fait l’affaire.

Et sinon, votre machine, que ce soit sur Windows, Linux ou macOS, tout est bon !

un compte bluesky dédié

  • Résultat de ce tuto visible sur ce bot : (sans les images parce que le site à la source ne support pas bien la technologie OpenGraph)
  • disclosengo.bsky.social

un “app password”

  • gardez-le au chaud pour plus tard

un flux RSS source du contenu à publier

le site Disclose.ngo est un site sur mesure, mais il propose 2 flux RSS

Pour trouver le flux RSS d’un site source, l’extension Feedbro est très utile on pourrait faire tout un article sur cette question, mais en général, il y a toujours moyen de récupérer le flux RSS d’un site, même s’il est bien caché !

bsky.rss

bsky.rss est un petit bijou de code qu’on peut trouver ici :

la commande git installée

Docker et Docker-Compose installé

mise en place

Je suis sur Linux Ubuntu, donc l’approche ici part de ce principe :

step 1

  • Créer un dossier /bsky-bot/
  • Cloner le dépôt à l’intérieur de ce dossier
git clone github.com/milanmdev/bsky.rss disclosengo

step 2

  • rentrer dans le dossier via votre terminal
cd disclosengo

structure

fichier et dossier important
  • docker-compose.example.yml
  • data/config.example.json
  • Dockerfile
ls disclosengo
  • Vous devriez voir un dossier data, le fichier compose et le fichier data/config.json
  • préparer vos fichiers de config
cp docker-compose.example.yml docker-compose.yml
cp data/config.example.json data/config.json

Mon compose file

Voici à quoi doit ressembler votre fichier docker-compose.yml une fois configuré

version: "3"
services:
  bsky-rss:
    restart: always # le conteneur se lance au démarrage
    image: bsky-queue:latest
    container_name: bsky-rss-disclosengo # le nom du container une fois qu'il tourne
    mem_limit: "256m" #mémoire limitée à
    mem_reservation: "128m" #mémoire réservée
    environment:
      - APP_PASSWORD=bqif-
      - INSTANCE_URL=https://bsky.social
      - FETCH_URL=https://disclose.ngo/feed?lang=en
      - IDENTIFIER=disclosengo.bsky.social
    volumes:
      - ./data:/build/data # le fameux dossier data dans le dépôt ? il sera utilisé comme volume d'écriture

Passons sur la branche de test pour bénéficier du système de file d’attente

git fetch --all
Fetching origin
  • par défaut, on est sur la branche main
disclosengo# git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
  remotes/origin/queue # on veut celle ci !!
  • on bouge sur la branche “queue”
disclosengo# git checkout queue
Branch 'queue' set up to track remote branch 'queue' from 'origin'.
Switched to a new branch 'queue'
disclosengo# 
  • on confirme qu’on est bien sur la bonne branche
git branch
  main
* queue
  • Par défaut le fichier docker-compose.yml va construire la dernière version stable de bsky.rss, mais nous on veut aller sur la version de dev, du coup on va la construire :
  • on va devoir changer une ligne dans notre fichier docker-compose.yml

ceci

image: ghcr.io/milanmdev/bsky.rss

devient :

image: bsky-queue:latest

on sauve le fichier et on fait :

docker build -t bsky-queue .

très important le point, il indique qu’on va utiliser le Dockerfile dans le dossier où l’on se trouve, ou se trouve le docker-compose.yml et le Dockerfile

Sending build context to Docker daemon    406kB
Step 1/8 : FROM node:lts
 ---> d9ad63743e72
Step 2/8 : LABEL org.opencontainers.image.description "A configurable RSS poster for Bluesky"
 ---> Using cache
 ---> 9899d560e894
Step 3/8 : LABEL org.opencontainers.image.source "https://github.com/milanmdev/bsky.rss"
 ---> Using cache
 ---> 3b9d7809df6e
Step 4/8 : WORKDIR /build
 ---> Using cache
 ---> d112e60329f1
Step 5/8 : COPY package.json yarn.lock ./
 ---> Using cache
 ---> 4d72a941e806
Step 6/8 : RUN yarn install --frozen-lockfile
 ---> Using cache
 ---> 8ce9a7982023
Step 7/8 : COPY . .
 ---> aa24c9bb0b84
Step 8/8 : CMD yarn start
 ---> Running in 8bcfa27fee9b
Removing intermediate container 8bcfa27fee9b
 ---> 4a3c1736110d
Successfully built 4a3c1736110d
Successfully tagged bsky-queue:latest

à ce stade vous avez la dernière image avec la version du code en phase de test, ce qui va nous permettre d’utiliser le système de file d’attente.

Ma config data/config.sjon
  • le lien va être intégré en carte, avec image intégrée grâce à PublishEmbed
  • en fonction du flux RSS, la description peut être utilisée pour générer un post plus long que juste le titre
  • configurer la langue en fonction du contenu du flux RSS
  • truncate c’est pour raccourcir le texte si la description est utilisée
  • le runInterval c’est toutes les minutes, il va checker s’il y a du contenu à publier
  • le datefield ne vous préoccupez pas avec ça pour le moment
{
  "string": "$title",
  "publishEmbed": true,
  "languages": ["en"],
  "truncate": true,
  "runInterval": 60,
  "dateField": ""
}
Options

Quand le “publishEmbed” est en “True”, il n’est pas nécéssaire d’ajouter la variable $link dans le champ string, qui correspond à ce qui va être posté, en effet la génération de la carte Embed, va se baser sur les meta OpenGraph du lien, l’image, la description et le titre qui y sont associés.

{
  "string": "$title - $link $description", # en général le titre suffit 
  "publishEmbed": true, # false déconseillé de mettre en false sauf si pas de lien
  "languages": ["en"], # fr, de, es, etc...
  "truncate": true, #false
  "runInterval": 60, # 120 absent de la branch main
  "dateField": "" # par défaut, il cherche pubDate sauf si le flux RSS n'est pas standard
}

step 3

  • à ce stade, vous êtes prêt pour lancer la bête !

Lancement

docker-compose up

D’abord le bot va remplir la file d’attente avec les posts dispo dans le flux RSS

:/home/bsky-bot/disclosengo# docker-compose up
[+] Running 1/1
 ⠿ Container bsky-rss-disclosengo  Created                                                                                                                                                                     0.2s
Attaching to bsky-rss-disclosengo
bsky-rss-disclosengo  | yarn run v1.22.19
bsky-rss-disclosengo  | $ tsx ./app/index.ts
bsky-rss-disclosengo  | [Mon, 14 Aug 2023 11:29:24 GMT] - [bsky.rss APP] Started RSS reader. Fetching from https://www.inoreader.com/stream/user/1005343511/tag/Disclosengo every 5 minutes.
[bsky.rss QUEUE] Starting queue handler. Running every 60 seconds
[bsky.rss QUEUE] Queuing item (Revealed: Perenco’s damaging oil spills in Gabon)
[bsky.rss QUEUE] Queuing item (Lützerath: French banks finance the extension of one of Europe’s largest coal mines )

À ce stade, le bot se déploie et vous devriez voir un retour dans votre terminal, si tout va bien, il va checker que le flux RSS et publier ce qu’il trouve à publier, ensuite le bot écrit dans un fichier txt (data/last.txt) la dernière fois qu’il a checker le flux RSS et va utiliser cette date pour comparer s’il y a du nouveau dans le flux RSS et ce toutes les 5 minutes.

[bsky.rss POST] Posting new item (Revealed: Perenco’s damaging oil spills in Gabon)
[bsky.rss POST] Posting new item (Lützerath: French banks finance the extension of one of Europe’s largest coal mines )
 [bsky.rss QUEUE] Finished running queue. Next run in 60 seconds
[bsky.rss QUEUE] Running queue with 20 items
[bsky.rss QUEUE] Finished running queue. Next run in 60 seconds
[bsky.rss QUEUE] Running queue with 0 items

Vous pouvez interrompre le bot avec CTRL+C, changer la config, changer le flux RSS, bref fignoler les détails et quand vous êtes content du résultat, vous lancez le bot avec

docker-compose up -d

Cela va lancer le bot comme un processus de tâche de fond.

Repasser sur la branche main, stable du code :

ceci dans le fichier docker-compose.yml

image: bsky-queue:latest

redevient :

image: ghcr.io/milanmdev/bsky.rss

on sauve le fichier et on fait :

docker-compose pull

on vérifie bien que le fichier config.json est bien adapté à la version du code

{
  "string": "$title", #vérifier la zone qui va construire le text du post
  "publishEmbed": true, #intégration des liens/images
  "languages": ["en"], #la,gues
  "truncate": true, # couper la description si trop longue
  "runInterval": 60, # si pas dans la branche queue
  "dateField": "" # champ date spécifique pour flux RSS non-standard
}

Bien faire attention que la dernière ligne de configuration du fichier config.json, ne doit pas comporter de virgule, mais toutes celles qui la précèdent bien !!

On lance la sauce :

docker-compose up -d

vérifier l’état du bot sur laydocker ou docker logs -f nom-de-votre-container

lazydocker

et voilà, vous êtes en train de faire tourner la version stable du bot !

Outils

  • J’utilise Lazydocker pour explorer les conteneurs qui tournent, le log, voir si tout va bien
  • docker-ctop est aussi pas mal pour explorer vite fait les bots qui tournent
  • Il y a moyen de “mixer” plusieurs flux RSS ensemble et ainsi avoir un bot multisources, mais ça, c’est pour un autre tuto !

Interesting tool for devs

Explore the #ActivityPub protocol interactively

ActivityPub.Academy is a learning resource for ActivityPub. The protocol is brought to life by showing Activities sent between different instances in real time!

activitypub.academy

Neat Comprehensive #Python Cheatsheet

gto76.github.io/python-ch…

Python & ActivityPub, initial research

Implementations

These are examples of ActivityPub server implementations written in Python:

Libraries

These are libraries that can help you with implementing ActivityPub:

Other Resources

Here are some Python libraries and resources that might be useful:

Remember to check each resource to see if it’s suitable for your project’s specific needs and constraints.