Créer un agrégateur de flux RSS alimenté par l'IA

· 41 min de lecture

En tant que MVP Microsoft et passionné de technologie, je me retrouve constamment noyé dans l’océan de contenu incroyable publié sur les DevBlogs de Microsoft. Des annonces .NET aux mises à jour de Visual Studio, des innovations Azure aux analyses approfondies du noyau sémantique, il se passe toujours quelque chose de nouveau et d’excitant dans l’écosystème Microsoft.

Le problème ? Suivre tout cela est presque impossible.

Je voulais rester au courant des dernières annonces et les partager avec mon réseau, mais vérifier manuellement sept flux RSS différents, lire des articles, créer des publications attrayantes sur les réseaux sociaux et suivre ce que j’avais déjà partagé devenait un travail à temps plein en soi. Chaque matin, j’ouvrais plusieurs onglets de navigateur, parcourais des dizaines d’articles, essayais de me souvenir de ceux que j’avais déjà partagés, puis passais un temps précieux à rédiger des articles sur ceux qui avaient retenu mon attention.

J’ai donc fait ce que n’importe quel développeur ferait : Je l’ai automatisé.

Dans ce guide complet, je vais vous expliquer comment j’ai créé un agrégateur de flux RSS alimenté par l’IA qui surveille plusieurs flux RSS Microsoft DevBlogs pour le nouveau contenu, utilise Azure OpenAI et Semantic Kernel pour analyser les articles et générer des publications attrayantes, crée une documentation de démarque détaillée pour chaque article analysé, envoie des notifications via Telegram afin que je puisse réviser et partager le contenu, tout suivre pour éviter les publications en double et s’exécute automatiquement via GitHub Actions.

Examinons en profondeur tous les aspects de cette solution.

L’histoire derrière ce projet

Vivre avec une surcharge d’informations

Laissez-moi vous dresser un tableau de ma matinée typique avant de créer cet outil. Je me réveillais, prenais mon café et ouvrais mon ordinateur portable pour vérifier les nouveautés de l’écosystème des développeurs Microsoft. Tout d’abord, je naviguerais vers le site principal de DevBlogs pour voir s’il y avait des annonces majeures. Ensuite, je consulterais le blog .NET spécifiquement parce que c’est ma principale pile technologique. Après cela, je me dirigerais vers le blog Semantic Kernel car l’IA devient de plus en plus importante. Le blog Visual Studio était le prochain sur la liste car les mises à jour de l’IDE peuvent avoir un impact significatif sur mon flux de travail quotidien. Vint ensuite le blog DevOps pour les actualités liées à CI/CD et GitHub, suivi du blog All Things Azure pour les mises à jour de l’infrastructure cloud, et enfin du blog Azure SQL pour les innovations en matière de bases de données.

Cela fait sept flux différents à vérifier. Chacun de ces blogs publie plusieurs articles par semaine, parfois plusieurs par jour lors des périodes d’annonces majeures comme .NET Conf ou Build. Cela représente potentiellement des dizaines d’articles à suivre, à lire et à partager. Et voici le problème : en tant que personne qui valorise le partage de connaissances avec la communauté, je ne voulais pas simplement lire ces articles. Je voulais partager les plus précieux avec mon réseau sur LinkedIn, aidant ainsi les autres développeurs à rester informés.Mais rédiger une bonne publication sur LinkedIn prend du temps. Vous devez lire attentivement l’article, comprendre les points clés, réfléchir aux raisons pour lesquelles il est important pour votre public, rédiger un texte engageant et bien formater le tout. Multipliez cela par plusieurs articles par semaine et vous obtenez des heures de travail.

Ce que je voulais vraiment

Après avoir réfléchi à ce problème pendant des mois, je me suis assis et j’ai réfléchi à ce à quoi ressemblerait une solution idéale. Avant tout, je ne voulais plus jamais manquer des annonces importantes. Le système devrait automatiquement détecter les nouveaux articles dès qu’ils sont publiés. Je voulais également gagner du temps sur la création de contenu en laissant l’IA m’aider à créer des publications engageantes – non pas pour remplacer entièrement ma voix, mais pour me donner un point de départ solide que je pourrais personnaliser.

La cohérence était un autre facteur important. Je voulais partager du contenu régulièrement sans avoir à penser à le faire manuellement chaque jour. L’aspect suivi était également crucial : j’avais besoin d’un moyen de savoir ce que j’avais déjà partagé pour éviter de publier des doublons et d’ennuyer mes abonnés. Enfin, je voulais rester organisé avec un enregistrement permanent de tout ce que j’ai traité, afin de pouvoir regarder en arrière et voir quels sujets j’ai abordés.

La solution prend forme

La solution que j’envisageais s’exécuterait selon un calendrier en utilisant GitHub Actions, entièrement mains libres. Il récupérerait automatiquement les sept flux sans que j’aie à ouvrir un seul onglet de navigateur. Le composant IA lirait et comprendrait le contenu, puis le résumerait d’une manière utile pour mon public. Au lieu de devoir écrire des articles à partir de zéro, cela créerait du contenu sur les réseaux sociaux prêt à partager que je pourrais modifier si nécessaire. Tout serait envoyé sur mon Telegram pour examen, afin que je puisse rapidement jeter un coup d’œil sur mon téléphone et décider quoi partager. Et bien sûr, il conserverait un enregistrement permanent de tout pour référence future.

Avant de commencer à construire

Ce dont vous aurez besoin sur votre machine

Pour suivre ce didacticiel, vous aurez besoin de quelques éléments installés sur votre machine de développement. Le plus important est le SDK .NET version 9.0 ou ultérieure. Il s’agit de notre environnement d’exécution et fournit tous les outils de construction dont nous avons besoin. Si vous ne l’avez pas installé, rendez-vous sur dot.net et téléchargez la dernière version. L’installation est simple sous Windows, macOS ou Linux.

Vous souhaiterez également que Git soit installé pour le contrôle de version. Nous allons transmettre notre code à GitHub et utiliser GitHub Actions pour l’automatisation. Il est donc essentiel de configurer Git localement. Toute version récente fonctionnera correctement.

Pour votre environnement de développement, je recommande Visual Studio ou VS Code. Personnellement, j’utilise VS Code pour la plupart de mon travail ces jours-ci car il est léger et offre un excellent support C# via l’extension C# Dev Kit. Mais si vous êtes plus à l’aise avec Visual Studio complet, cela fonctionne également parfaitement.

Services et comptes dont vous aurez besoinAu-delà des outils locaux, vous aurez besoin de comptes avec quelques services. Le plus important est Azure OpenAI, qui alimente notre analyse de l’IA. Il s’agit d’un service payant, mais les coûts sont minimes pour ce cas d’utilisation : nous parlons de centimes par article analysé. Si vous n’avez pas de compte Azure, vous pouvez vous inscrire pour un essai gratuit qui comprend des crédits pour commencer.

Pour les notifications, nous utiliserons un Telegram Bot. L’avantage de Telegram est que son API de bot est totalement gratuite. Vous pouvez créer autant de robots que vous le souhaitez et envoyer des messages illimités. Je vous expliquerai le processus de configuration plus loin dans ce guide.

Enfin, vous aurez besoin d’un compte GitHub pour héberger votre code et exécuter GitHub Actions. Le niveau gratuit est plus que suffisant pour ce projet. GitHub vous offre 2 000 minutes d’exécution d’actions par mois sur les référentiels privés et un nombre illimité de minutes sur les référentiels publics.

Les bibliothèques qui rendent cela possible

Notre projet s’appuie sur trois packages NuGet principaux, chacun servant un objectif spécifique.

Le premier est HtmlAgilityPack, qui est la référence en matière d’analyse HTML dans .NET. Lorsque nous récupérons un article d’un blog, nous récupérons le code HTML complet de la page, y compris les menus de navigation, les pieds de page, les publicités et toutes sortes d’éléments qui ne nous intéressent pas. HtmlAgilityPack nous permet d’analyser ce code HTML et d’extraire uniquement le contenu de l’article dont nous avons besoin.

Le deuxième package est Microsoft.SemanticKernel, qui est le SDK de Microsoft permettant d’intégrer des modèles d’IA dans des applications. Considérez-le comme le pont entre votre code .NET et les grands modèles de langage comme GPT-4. Il gère toute la complexité des appels d’API, de la gestion des jetons et de l’analyse des réponses, vous permettant de vous concentrer sur ce que vous voulez que l’IA fasse réellement.

Le troisième package est System.ServiceModel.Syndication, qui fournit une prise en charge intégrée pour l’analyse des flux RSS et Atom. RSS peut sembler une technologie ancienne, mais il reste le meilleur moyen d’obtenir des mises à jour structurées à partir de blogs et de sites d’actualités. Ce package transforme les flux XML bruts en objets C# fortement typés et faciles à utiliser.

Comprendre l’architecture

Comment les pièces s’emboîtent

Avant de plonger dans le code, laissez-moi vous expliquer comment tous les composants fonctionnent ensemble. Comprendre la situation dans son ensemble rendra les détails de mise en œuvre beaucoup plus clairs.

Au plus haut niveau, nous avons notre fichier principal Program.cs qui fait office d’orchestrateur. C’est le point d’entrée de notre application, et il coordonne tous les autres composants. Lorsque l’application s’exécute, elle charge d’abord la configuration à partir des variables d’environnement, telles que les clés API et les informations d’identification Telegram. Ensuite, il récupère les flux RSS des sept sources Microsoft DevBlogs. Lors du traitement de ces flux, il déduplique les articles pour gérer les cas où le même article apparaît dans plusieurs flux. Il vérifie chaque article par rapport à notre fichier de suivi pour voir si nous l’avons déjà traité. Pour les nouveaux articles, il les transmet à l’analyseur IA pour traitement.La classe ArticleAnalyzer est l’endroit où la magie de l’IA se produit. Ce composant reçoit un article et en fait plusieurs choses. Tout d’abord, il récupère l’intégralité du contenu HTML à partir de l’URL de l’article. Ensuite, il extrait le texte propre de ce code HTML, en supprimant tous les éléments de navigation, scripts et styles dont nous n’avons pas besoin. Une fois qu’il dispose d’un texte clair, il l’envoie à Azure OpenAI via Semantic Kernel avec une invite soigneusement conçue. L’IA analyse l’article et renvoie une réponse structurée qui comprend un résumé, des sujets clés, une explication de pertinence et, surtout, une publication LinkedIn prête à l’emploi. L’analyseur analyse cette réponse et renvoie un objet ArticleAnalysis contenant toutes ces informations.

La classe MarkdownGenerator prend cet objet ArticleAnalysis et en crée un enregistrement permanent. Il génère un fichier de démarque bien formaté qui comprend toutes les métadonnées de l’article, l’analyse de l’IA et la publication générée. Ces fichiers sont stockés dans un répertoire de publications générées, vous offrant une archive consultable de tout ce que vous avez traité.

Enfin, l’intégration Telegram envoie le contenu de la publication généré à votre téléphone. C’est à ce moment-là que vous, en tant qu’humain, pouvez examiner le travail de l’IA et décider de le partager ou non. Le bot vous envoie un message avec le contenu de la publication, et vous pouvez soit le copier directement sur LinkedIn, soit le modifier d’abord.

Le flux de données

Laissez-moi vous expliquer ce qui se passe lorsqu’un nouvel article est publié sur le blog .NET. Le workflow démarre lorsque GitHub Actions déclenche notre application selon son calendrier, disons toutes les six heures. L’application se réveille et commence à récupérer les sept flux RSS. Chaque flux renvoie un document XML contenant les articles les plus récents de ce blog.

Au fur et à mesure que nous analysons chaque flux, nous extrayons des articles individuels et les stockons dans une liste. Mais voici une partie délicate : le flux principal des DevBlogs comprend souvent des articles qui apparaissent également dans les flux de catégories individuelles. Ainsi, un article sur « .NET 10 » peut apparaître à la fois dans le flux principal et dans le flux spécifique à .NET. Nous gérons cela en suivant les URL dans un HashSet, ce qui empêche automatiquement les doublons.

Une fois que nous avons notre liste d’articles dédupliquée, nous la filtrons uniquement pour les plus récents – généralement les articles publiés au cours des derniers jours. Nous ne voulons pas traiter d’anciens articles que nous avons déjà traités lors d’exécutions précédentes. Ensuite, nous vérifions chaque article récent par rapport à notre fichier de suivi. Si nous avons déjà traité et publié un article, nous l’ignorons.

Pour chaque nouvel article, nous lançons le pipeline d’analyse de l’IA. L’analyseur récupère le code HTML complet de l’article, le nettoie et l’envoie à GPT-4 avec notre invite. L’IA lit l’article et génère une analyse complète ainsi qu’une publication LinkedIn. Nous enregistrons cette analyse dans un fichier de démarque pour nos dossiers.Une fois l’analyse terminée, nous formatons un message et l’envoyons via Telegram. Le message inclut le contenu de la publication généré avec l’URL et les hashtags ajoutés. Sur mon téléphone, je reçois une notification, je consulte la publication et si je l’aime, je peux la copier et la partager sur LinkedIn en quelques clics.

Enfin, nous mettons à jour notre fichier de suivi pour marquer cet article comme traité, afin de ne plus le traiter lors des prochaines exécutions. Si des fichiers ont été créés ou modifiés, GitHub Actions valide ces modifications dans le référentiel, en gardant tout synchronisé.

Configurer le projet à partir de zéro

Création de la structure de la solution

Commençons à construire. Ouvrez votre terminal et accédez à l’endroit où vous souhaitez créer le projet. J’aime garder mes projets organisés dans un dossier Développement, mais vous pouvez le placer là où cela vous convient.

Tout d’abord, nous allons créer un nouveau fichier de solution. Dans .NET, une solution est un conteneur pouvant contenir plusieurs projets. Même si nous n’avons qu’un seul projet pour l’instant, commencer par une solution facilite l’ajout de projets supplémentaires ultérieurement si nécessaire. Exécutez la commande dotnet new sln -n vs-feed-linkedin pour créer une solution nommée vs-feed-linkedin.

Ensuite, nous devons créer notre projet d’application console. Nous allons mettre cela dans un sous-répertoire src pour garder les choses organisées. Exécutez dotnet new console -n VsFeedLinkedin -o src pour créer un projet de console nommé VsFeedLinkedin dans le dossier src. Ajoutez ensuite ce projet à notre solution avec dotnet sln add src/VsFeedLinkedin.csproj.

Naviguez maintenant dans le répertoire src avec cd src. C’est ici que nous ajouterons nos packages NuGet et effectuerons l’essentiel de notre développement.

Ajout des packages requis

Une fois notre projet créé, nous devons ajouter les trois packages NuGet que j’ai mentionnés plus tôt. Exécutez chacune de ces commandes dans l’ordre :

dotnet add package System.ServiceModel.Syndication --version 9.0.9
dotnet add package Microsoft.SemanticKernel --version 1.30.0
dotnet add package HtmlAgilityPack --version 1.11.72

Après avoir exécuté ces commandes, votre fichier de projet devrait ressembler à ceci :

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
    <PackageReference Include="Microsoft.SemanticKernel" Version="1.30.0" />
    <PackageReference Include="System.ServiceModel.Syndication" Version="9.0.9" />
  </ItemGroup>

</Project>

Le fichier de projet indique à .NET que nous construisons un exécutable (OutputType est Exe), ciblant .NET 9.0 et utilisant des fonctionnalités C# modernes telles que les utilisations implicites et les types de référence nullables. La section ItemGroup répertorie nos trois dépendances de packages avec leurs versions exactes.

Plongez en profondeur dans les flux RSS

Qu’est-ce que RSS exactement ?

Avant de commencer à écrire du code pour récupérer des flux, assurons-nous de bien comprendre avec quoi nous travaillons. RSS signifie Really Simple Syndication et il s’agit d’un format XML standardisé pour la distribution de mises à jour de contenu. L’idée est simple : au lieu d’obliger les utilisateurs à visiter votre site Web pour voir s’il y a du nouveau contenu, vous publiez un fichier lisible par machine qui répertorie votre contenu récent. Les applications peuvent ensuite interroger périodiquement ce fichier pour découvrir de nouveaux articles.

Le RSS existe depuis la fin des années 1990 et le début des années 2000. Vous pensez peut-être qu’il s’agit d’une technologie obsolète, mais elle est en réalité encore largement utilisée, notamment par les blogs, les sites d’actualités et les podcasts. La beauté du RSS réside dans sa simplicité. C’est juste du XML avec une structure définie, et n’importe quelle application peut l’analyser.

La structure d’un flux DevBlogsLorsque vous récupérez un flux RSS depuis Microsoft DevBlogs, vous obtenez un document XML qui suit une structure spécifique. Au niveau supérieur, il y a un élément rss qui contient un seul élément de canal. Le canal représente le blog lui-même et inclut des métadonnées telles que le titre, l’URL et la description du blog.

À l’intérieur de la chaîne, vous trouverez plusieurs éléments d’élément, chacun représentant un article de blog individuel. Chaque élément comprend un titre (le titre de l’article), un lien (l’URL où vous pouvez lire l’article complet), une pubDate (la date de publication de l’article), un élément dc:creator (le nom de l’auteur), un ou plusieurs éléments de catégorie (balises pour l’article) et une description (généralement un résumé ou un extrait de l’article).

Voici un exemple simplifié de ce à quoi cela ressemble :

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>.NET Blog</title>
    <link>https://devblogs.microsoft.com/dotnet</link>
    <description>The latest news about .NET</description>
    <item>
      <title>Announcing .NET 10</title>
      <link>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10</link>
      <pubDate>Mon, 10 Dec 2025 12:00:00 GMT</pubDate>
      <dc:creator>Microsoft</dc:creator>
      <category>Announcements</category>
      <category>.NET</category>
      <description>Article summary...</description>
    </item>
  </channel>
</rss>

L’avantage du package System.ServiceModel.Syndication de .NET est qu’il analyse tout cela pour nous. Nous n’avons pas besoin de naviguer manuellement dans les nœuds XML ni de nous soucier des différentes versions RSS. Nous chargeons simplement le flux et récupérons les objets fortement typés.

Les sept flux que nous surveillons

Dans mon implémentation, je surveille sept flux Microsoft DevBlogs différents. Le flux principal DevBlogs sur devblogs.microsoft.com/feed nous donne une vue d’ensemble de tout ce que Microsoft publie sur tous ses blogs de développeurs. Le flux spécifique à .NET sur devblogs.microsoft.com/dotnet/feed se concentre spécifiquement sur les versions, les fonctionnalités et les meilleures pratiques de .NET. Le flux Semantic Kernel sur devblogs.microsoft.com/semantic-kernel/feed couvre l’orchestration et l’intégration de l’IA – de plus en plus importantes à mesure que l’IA devient centrale dans le développement moderne.

Le flux Visual Studio sur devblogs.microsoft.com/visualstudio/feed me tient au courant des améliorations de l’IDE et des fonctionnalités de productivité. Le flux DevOps sur devblogs.microsoft.com/devops/feed couvre les sujets Azure DevOps, GitHub et CI/CD. Le flux All Things Azure sur devblogs.microsoft.com/all-things-azure/feed se concentre sur les services cloud et les modèles d’architecture. Enfin, le flux Azure SQL sur devblogs.microsoft.com/azure-sql/feed couvre les innovations et fonctionnalités des bases de données.

Vous vous demandez peut-être pourquoi je vérifie à la fois le flux principal et les flux de catégories individuelles. Le flux principal me donne de l’ampleur : je verrai des articles de n’importe quel blog de développeur Microsoft, y compris ceux que je ne connais peut-être pas. Les flux de catégories me donnent de la profondeur : ils garantissent que je ne manque rien d’important dans mes principaux domaines d’intérêt, même si ces articles sont exclus du flux principal par du contenu plus récent.

Construire la logique de récupération RSS

La fonction de récupération de base

Maintenant, écrivons du code. La base de notre application est la capacité de récupérer et d’analyser les flux RSS. Voici la fonction qui gère cela :

static async Task<SyndicationFeed?> FetchRssFeedAsync(string url)
{
    using var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
    
    var response = await httpClient.GetStringAsync(url);
    
    using var stringReader = new StringReader(response);
    
    var settings = new XmlReaderSettings
    {
        DtdProcessing = DtdProcessing.Parse,
        MaxCharactersFromEntities = 1024
    };
    
    using var xmlReader = XmlReader.Create(stringReader, settings);
    
    return SyndicationFeed.Load(xmlReader);
}

Laissez-moi vous expliquer ce que fait ce code. Nous commençons par créer un HttpClient, qui est la classe intégrée de .NET pour effectuer des requêtes HTTP. Nous définissons un en-tête User-Agent car certains serveurs bloquent les requêtes qui ne s’identifient pas. C’est une bonne pratique de définir cela même lorsque les serveurs ne l’exigent pas.Nous effectuons ensuite une requête GET à l’URL du flux et recevons la réponse sous forme de chaîne. Cette chaîne contient le XML brut du flux RSS.

Pour analyser ce XML, nous créons un StringReader pour envelopper notre chaîne de réponse, puis configurons certains XmlReaderSettings. Le paramètre DtdProcessing est important : les flux RSS incluent parfois des déclarations DTD (Document Type Definition) qui doivent être traitées. Le paramètre MaxCharactersFromEntities est une mesure de sécurité qui empêche les attaques à la bombe XML en limitant le degré d’expansion des entités pouvant se produire.

Enfin, nous créons un XmlReader avec ces paramètres et utilisons SyndicationFeed.Load pour analyser le XML en un objet SyndicationFeed fortement typé. Cela nous donne accès aux métadonnées du flux et à tous ses éléments via de jolies propriétés C# au lieu d’une navigation XML brute.

Récupération de plusieurs flux avec gestion des erreurs

Dans le monde réel, les requêtes réseau échouent. Les serveurs tombent en panne, les connexions expirent et XML peut être mal formé. Nous devons traiter ces cas avec élégance. Voici comment nous récupérons tous nos flux tout en étant résilients aux pannes :

var allArticles = new List<(SyndicationItem item, string feedUrl)>();
var seenUrls = new HashSet<string>();

foreach (var feedUrl in feedUrls)
{
    try
    {
        Console.WriteLine($"  📡 Fetching {feedUrl}...");
        var feed = await FetchRssFeedAsync(feedUrl);
        
        if (feed?.Items != null && feed.Items.Any())
        {
            foreach (var item in feed.Items)
            {
                var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? "";
                if (!string.IsNullOrEmpty(itemUrl) && seenUrls.Add(itemUrl))
                {
                    allArticles.Add((item, feedUrl));
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  ⚠️  Failed to fetch {feedUrl}: {ex.Message}");
    }
}

Nous conservons ici deux collections. La liste allArticles contiendra tous les articles que nous trouvons, ainsi que le flux d’où ils proviennent. Le sawUrls HashSet suit les URL d’articles que nous avons déjà vues, ce qui nous aide à éviter les doublons.

Nous parcourons chaque URL de flux et encapsulons l’opération de récupération dans un bloc try-catch. Si la récupération d’un flux particulier échoue (peut-être que le serveur est temporairement en panne), nous enregistrons un avertissement et passons au flux suivant. Ainsi, un problème sur un flux ne nous empêche pas de traiter les autres.

Pour chaque flux récupéré avec succès, nous parcourons ses éléments. Nous extrayons l’URL de l’article de la collection Links de l’élément. La méthode HashSet.Add renvoie false si l’URL est déjà dans l’ensemble, ce qui est parfait pour notre logique de déduplication. Nous ajoutons l’article à notre liste uniquement s’il est nouveau.

Nous stockons l’URL du flux à côté de chaque article, car ces informations pourraient être utiles ultérieurement. Par exemple, nous pourrions vouloir savoir de quel flux spécifique provient un article à des fins de débogage ou de journalisation.

Gestion des doublons et suivi de l’état

Le défi de la déduplication

Comme je l’ai mentionné plus tôt, Microsoft DevBlogs a une structure de flux hiérarchique qui crée un défi intéressant. Lorsqu’un membre de l’équipe .NET publie un article sur, par exemple, les améliorations des performances dans .NET 10, cet article apparaîtra probablement à la fois dans le flux principal DevBlogs et dans le flux spécifique à .NET. Parfois, il peut même apparaître dans le flux Visual Studio s’il concerne les fonctionnalités de l’EDI.

Si nous traitions naïvement chaque article de chaque flux, nous finirions par analyser et publier plusieurs fois le même article. Cela gaspillerait les appels d’API vers Azure OpenAI, spammerait notre Telegram avec des notifications en double et potentiellement ennuyer nos abonnés si nous publiions des doublons.La solution est la déduplication basée sur les URL. Chaque article a une URL unique, nous pouvons donc l’utiliser comme identifiant. La structure de données HashSet est parfaite pour cela car elle fournit un temps de recherche O(1) et empêche automatiquement les doublons. Lorsque nous essayons d’ajouter une URL qui figure déjà dans l’ensemble, la méthode Add renvoie simplement false, nous indiquant que nous devons ignorer cet article.

État persistant avec Markdown

La déduplication gère les doublons au sein d’une seule exécution, mais qu’en est-il entre les exécutions ? Lorsque notre application s’exécute toutes les six heures, nous devons nous souvenir des articles que nous avons déjà traités afin de ne plus les traiter.

J’ai choisi de stocker cet état dans un fichier markdown appeléposted-articles.md. Pourquoi une démarque ? Quelques raisons. Premièrement, c’est lisible par l’homme. Je peux ouvrir le fichier et voir immédiatement quels articles j’ai partagés. Deuxièmement, il est contrôlé par la version. Étant donné que ce fichier se trouve dans notre référentiel Git, j’ai un historique complet du moment où les articles ont été traités. Troisièmement, il sert de documentation. Quiconque consulte le référentiel peut voir ce que l’application a fait.

Le format de ce fichier est simple. Il comporte un en-tête, un horodatage indiquant la dernière exécution de l’application, puis une liste d’articles au format lien markdown :

# Posted Articles

*Last run: 2025-12-10 15:30:00*

List of articles posted to LinkedIn:

- [Announcing .NET 10](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
- [Visual Studio 2026 Preview](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)

Chargement et analyse du fichier de suivi

Pour vérifier si nous avons déjà traité un article, nous devons charger ce fichier et extraire les URL. Voici la fonction qui fait cela :

static HashSet<string> LoadPostedArticles(string filePath)
{
    var postedUrls = new HashSet<string>();
    
    if (!File.Exists(filePath))
    {
        return postedUrls;
    }

    var lines = File.ReadAllLines(filePath);
    foreach (var line in lines)
    {
        var match = System.Text.RegularExpressions.Regex.Match(line, @"\(([^)]+)\)");
        if (match.Success)
        {
            var url = match.Groups[1].Value;
            
            if (url.Contains("?wt.mc_id="))
            {
                url = url.Substring(0, url.IndexOf("?wt.mc_id="));
            }
            else if (url.Contains("?"))
            {
                url = url.Substring(0, url.IndexOf("?"));
            }
            
            url = url.TrimEnd('/');
            postedUrls.Add(url);
        }
    }

    return postedUrls;
}

Cette fonction renvoie un HashSet contenant toutes les URL que nous avons déjà traitées. Nous commençons par vérifier si le fichier existe – lors de la première exécution, ce ne sera pas le cas, nous renvoyons donc un ensemble vide.

Pour chaque ligne du fichier, nous utilisons une regex pour extraire l’URL du format de lien markdown. L’expression régulière \(([^)]+)\) correspond à tout ce qui se trouve entre parenthèses, où les liens de démarque stockent leurs URL.

Vient ensuite une étape importante : la normalisation des URL. Les URL d’un même article peuvent varier en format. Le flux RSS peut nous donner https://devblogs.microsoft.com/dotnet/article, mais notre version enregistrée comporte un paramètre de suivi ajouté : https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972. Certaines URL comportent des barres obliques finales, d’autres non.

Pour gérer cela, nous supprimons tous les paramètres de requête (tout ce qui se trouve après le ?) et supprimons les barres obliques finales. Cette normalisation garantit que nous reconnaissons les articles comme des doublons même si leurs URL diffèrent de manière superficielle.

Sauvegarde des nouveaux articles

Lorsque nous traitons avec succès un article, nous devons l’ajouter à notre fichier de suivi :

static void SavePostedArticle(string filePath, string url, string title, DateTimeOffset publishDate)
{
    var markdownEntry = $"- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n";
    
    if (!File.Exists(filePath))
    {
        File.WriteAllText(filePath, "# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n");
    }
    
    File.AppendAllText(filePath, markdownEntry);
}

Cette fonction crée une entrée au format markdown avec le titre de l’article comme lien, suivi d’horodatages indiquant quand nous l’avons publié et quand il a été initialement publié. Si le fichier n’existe pas encore, nous le créons d’abord avec un en-tête.

Le moteur d’analyse de l’IA

Comprendre le noyau sémantiquePassons maintenant à la partie la plus intéressante de notre application : l’analyse de l’IA. Semantic Kernel est le SDK open source de Microsoft permettant d’intégrer de grands modèles de langage dans des applications. C’est plus qu’un simple wrapper autour des appels API. Il fournit un cadre pour créer des applications d’IA sophistiquées avec des fonctionnalités telles que des plugins, des planificateurs et de la mémoire.

Pour notre cas d’utilisation, nous utilisons les capacités de complétion de discussion de Semantic Kernel. Nous enverrons une invite à Azure OpenAI, et le modèle analysera notre article et générera une réponse. Semantic Kernel gère toute la complexité de l’authentification API, du formatage des requêtes et de l’analyse des réponses.

Configuration de l’analyseur d’articles

Voyons comment nous avons configuré notre classe d’analyseur :

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using HtmlAgilityPack;

namespace VsFeedLinkedin.Services;

public class ArticleAnalyzer
{
    private readonly Kernel _kernel;
    private readonly IChatCompletionService _chatService;

    public ArticleAnalyzer(string endpoint, string apiKey, string deploymentName)
    {
        var builder = Kernel.CreateBuilder();
        
        builder.AddAzureOpenAIChatCompletion(
            deploymentName: deploymentName,
            endpoint: endpoint,
            apiKey: apiKey
        );
        
        _kernel = builder.Build();
        _chatService = _kernel.GetRequiredService<IChatCompletionService>();
    }

Le noyau sémantique utilise un modèle de générateur pour la configuration. Nous créons un KernelBuilder, ajoutons notre service de complétion de chat Azure OpenAI avec les informations d’identification nécessaires, puis créons le noyau. À partir du noyau construit, nous récupérons l’interface IChatCompletionService, que nous utiliserons pour envoyer des invites et recevoir des réponses.

Le constructeur prend trois paramètres : le point de terminaison Azure OpenAI (quelque chose comme https://your-resource.openai.azure.com/), votre clé API et le nom du déploiement (comme gpt-4o). Celles-ci sont transmises à partir de variables d’environnement, garantissant ainsi la sécurité de nos informations d’identification.

Créer l’invite parfaite

L’invite que nous envoyons à l’IA est cruciale. Une invite bien conçue produit des résultats cohérents et de haute qualité. Une invite vague ou mal structurée produit des résultats incohérents et médiocres. J’ai passé beaucoup de temps à parcourir cette invite pour obtenir des résultats qui me satisfont :

var prompt = $"""
    You are a professional tech content analyst and LinkedIn content creator. 
    Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.

    Article Title: {title}
    Author: {author}
    URL: {url}
    Tags: {string.Join(", ", tags)}
    
    Article Content:
    {cleanContent}

    Please provide:
    1. A brief summary (2-3 sentences) of the key points
    2. The main technologies or topics covered
    3. Why this is relevant for developers/tech professionals
    4. An engaging LinkedIn post (max 1300 characters) that:
       - Starts with a hook or attention-grabbing statement
       - Highlights the key value for readers
       - Includes a call to action
       - Uses appropriate emojis (but not too many)
       - Maintains a professional yet approachable tone
       - DO NOT include hashtags in the post (they will be added separately)
       - DO NOT include the URL in the post (it will be added separately)

    Format your response as follows:
    ## Summary
    [Your summary here]

    ## Key Topics
    [List of main topics/technologies]

    ## Relevance
    [Why this matters]

    ## LinkedIn Post
    [Your engaging LinkedIn post here]
    """;

Laissez-moi vous expliquer les décisions de conception ici. Nous commençons par donner un rôle clair à l’IA : « Vous êtes un analyste professionnel de contenu technologique et un créateur de contenu LinkedIn. » Cela prépare le modèle à répondre avec le style et la voix appropriés.

Nous fournissons tout le contexte dont l’IA a besoin : le titre de l’article, l’auteur, l’URL, les balises du flux RSS et le contenu complet de l’article. Plus nous donnons de contexte, meilleure sera l’analyse.

Ensuite, nous précisons exactement ce que nous voulons en retour. Je demande quatre choses : un résumé, des sujets clés, une explication de la pertinence et une publication sur LinkedIn. Pour la publication LinkedIn en particulier, je donne des instructions détaillées sur ce qui fait une bonne publication : elle doit avoir une accroche, mettre en valeur la valeur, inclure un appel à l’action, utiliser les emojis de manière appropriée et conserver un ton professionnel.

Les instructions négatives sont tout aussi importantes. Je dis explicitement à l’IA de NE PAS inclure de hashtags ou d’URL dans la publication. Pourquoi? Parce que je les ajoute séparément, et si l’IA les incluait, j’aurais des doublons. Ce type d’instruction explicite évite les erreurs courantes.

Enfin, je précise le format de sortie exact. En demandant des sections marquées d’en-têtes ##, je rends la réponse facile à analyser par programme. L’IA est très douée pour suivre les instructions de formatage, et cette cohérence rend notre code d’analyse plus simple et plus fiable.

Exécution de l’analyse

Voici comment nous assemblons tout cela :

public async Task<ArticleAnalysis> AnalyzeArticleAsync(
    string title, 
    string url, 
    string htmlContent, 
    string author, 
    List<string> tags)
{
    var cleanContent = ExtractTextFromHtml(htmlContent);
    
    if (cleanContent.Length > 8000)
    {
        cleanContent = cleanContent.Substring(0, 8000) + "...";
    }

    var chatHistory = new ChatHistory();
    chatHistory.AddUserMessage(prompt);

    var response = await _chatService.GetChatMessageContentAsync(chatHistory);
    var responseText = response.Content ?? "";

    return ParseAnalysisResponse(responseText, title, url, author, tags);
}
```Nous extrayons dabord le texte propre du contenu HTML (jexpliquerai cela dans la section suivante). Ensuite, nous tronquons le contenu s'il est trop long. Les grands modèles de langage ont des limites symboliques, et les articles très longs peuvent les dépasser. En plafonnant à 8 000 caractères, nous nous assurons de rester dans les limites tout en fournissant un contexte substantiel.

Nous créons un objet ChatHistory et ajoutons notre invite en tant que message utilisateur. Il s'agit de l'abstraction de Semantic Kernel pour les interactions basées sur le chat. Nous l'envoyons au service de complétion de chat et obtenons une réponse. Enfin, nous analysons la réponse pour extraire les sections individuelles.

### Analyse de la réponse de l'IA

L'IA renvoie sa réponse sous forme de texte formaté avec la structure demandée. Nous devons analyser cela en champs individuels :

```csharp
private static ArticleAnalysis ParseAnalysisResponse(
    string response, 
    string title, 
    string url, 
    string author, 
    List<string> tags)
{
    var analysis = new ArticleAnalysis
    {
        Title = title,
        Url = url,
        Author = author,
        Tags = tags,
        RawAnalysis = response
    };

    var sections = response.Split("##", StringSplitOptions.RemoveEmptyEntries);
    
    foreach (var section in sections)
    {
        var lines = section.Trim().Split('\n', 2);
        if (lines.Length < 2) continue;
        
        var sectionTitle = lines[0].Trim().ToLower();
        var sectionContent = lines[1].Trim();

        switch (sectionTitle)
        {
            case "summary":
                analysis.Summary = sectionContent;
                break;
            case "key topics":
                analysis.KeyTopics = sectionContent;
                break;
            case "relevance":
                analysis.Relevance = sectionContent;
                break;
            case "linkedin post":
                analysis.LinkedInPost = sectionContent;
                break;
        }
    }

    return analysis;
}

Nous divisons la réponse par les marqueurs ##, ce qui nous donne chaque section. Pour chaque section, nous divisons par nouvelle ligne pour séparer l’en-tête du contenu. Nous utilisons ensuite une instruction switch pour attribuer le contenu de chaque section à la propriété appropriée.

Nous stockons également la réponse brute et non analysée. Ceci est utile pour le débogage – si quelque chose ne va pas avec l’analyse, nous pouvons regarder ce que l’IA a réellement renvoyé.

Extraction de contenu à partir de HTML

Pourquoi nous devons nettoyer le HTML

Lorsque nous récupérons un article d’un blog, nous obtenons le code HTML complet de la page. Cela inclut bien plus que le contenu de l’article : il existe des menus de navigation, des en-têtes, des pieds de page, des barres latérales, des widgets d’articles associés, des sections de commentaires, des scripts d’analyse et de suivi, des feuilles de style et toutes sortes d’autres éléments.

Si nous envoyions tout cela à notre IA, plusieurs mauvaises choses se produiraient. L’IA devrait traiter beaucoup de texte non pertinent, gaspillant des jetons et potentiellement confondre l’analyse. Le texte de navigation et de pied de page peut être inclus dans le résumé. Les scripts et CSS seraient traités comme du contenu, ce qui polluerait davantage l’analyse.

Nous devons extraire uniquement le contenu de l’article – la partie qu’un lecteur humain lirait réellement.

Utilisation de HtmlAgilityPack

HtmlAgilityPack est une bibliothèque d’analyse HTML robuste pour .NET. Contrairement à XML, le HTML est souvent mal formé : les balises peuvent ne pas être correctement fermées, les attributs peuvent ne pas être correctement cités. HtmlAgilityPack gère tout cela avec élégance, nous donnant une structure de type DOM que nous pouvons interroger et manipuler.

Voici notre fonction d’extraction :

private static string ExtractTextFromHtml(string html)
{
    if (string.IsNullOrWhiteSpace(html))
        return string.Empty;

    var doc = new HtmlDocument();
    doc.LoadHtml(html);

    var nodesToRemove = doc.DocumentNode.SelectNodes("//script|//style|//nav|//footer|//header");
    if (nodesToRemove != null)
    {
        foreach (var node in nodesToRemove)
        {
            node.Remove();
        }
    }

    var text = doc.DocumentNode.InnerText;
    
    text = System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ");
    return text.Trim();
}

Nous chargeons le HTML dans un HtmlDocument, qui l’analyse dans une structure arborescente. Ensuite, nous utilisons XPath pour sélectionner tous les nœuds que nous souhaitons supprimer. L’expression XPath //script|//style|//nav|//footer|//header sélectionne tous les éléments de script (code JavaScript dont nous n’avons pas besoin), les éléments de style (CSS dont nous n’avons pas besoin), les éléments de navigation (menus de navigation), les éléments de pied de page et les éléments d’en-tête.

Après avoir supprimé ces nœuds, nous obtenons la propriété InnerText, qui extrait tout le contenu du texte tout en supprimant les balises HTML. Cela nous donne le texte brut de l’article.Enfin, nous nettoyons les espaces. Le HTML comporte souvent de nombreux espaces supplémentaires à des fins de formatage : plusieurs espaces, tabulations, nouvelles lignes. Nous utilisons une expression régulière pour remplacer toute séquence de caractères d’espacement par un seul espace, puis coupons le résultat.

Récupération de l’article complet

Le flux RSS ne nous donne que des résumés, pas le contenu complet de l’article. Pour obtenir le texte complet, nous devons récupérer la page Web de l’article :

public static async Task<string> FetchArticleContentAsync(string url)
{
    using var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
    
    try
    {
        return await httpClient.GetStringAsync(url);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"⚠️ Failed to fetch article content: {ex.Message}");
        return string.Empty;
    }
}

C’est simple : nous envoyons une requête HTTP GET à l’URL de l’article et renvoyons la réponse HTML. Nous l’enveloppons dans un try-catch car les requêtes réseau peuvent échouer et nous préférons renvoyer une chaîne vide plutôt que de planter toute l’application.

Création d’une documentation permanente

Pourquoi générer des fichiers Markdown

Chaque fois que nous analysons un article, nous générons un fichier de démarque détaillé documentant cette analyse. Cela répond à plusieurs objectifs.

Premièrement, il crée une archive consultable. Au fil du temps, vous constituerez une collection d’articles analysés. Vous pouvez effectuer une recherche dans ces fichiers pour trouver du contenu antérieur sur des sujets spécifiques.

Deuxièmement, cela assure la transparence. Vous pouvez voir exactement ce que l’IA a généré pour chaque article, y compris l’analyse complète et la publication LinkedIn.

Troisièmement, c’est utile pour le débogage. Si quelque chose ne va pas avec une publication, vous pouvez consulter le fichier de démarque pour comprendre ce qui s’est passé.

La classe du générateur de démarques

public class MarkdownGenerator
{
    private readonly string _outputDirectory;

    public MarkdownGenerator(string outputDirectory)
    {
        _outputDirectory = outputDirectory;
        
        if (!Directory.Exists(_outputDirectory))
        {
            Directory.CreateDirectory(_outputDirectory);
        }
    }

    public string GenerateMarkdownFile(ArticleAnalysis analysis)
    {
        var sb = new StringBuilder();
        
        var safeTitle = GenerateSafeFileName(analysis.Title);
        var fileName = $"{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md";
        var filePath = Path.Combine(_outputDirectory, fileName);

        sb.AppendLine($"# {analysis.Title}");
        sb.AppendLine();
        sb.AppendLine("## Article Information");
        sb.AppendLine();
        sb.AppendLine($"- **Author:** {analysis.Author}");
        sb.AppendLine($"- **URL:** [{analysis.Url}]({analysis.Url})");
        sb.AppendLine($"- **Published:** {analysis.PublishDate:yyyy-MM-dd}");
        sb.AppendLine($"- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}");
        sb.AppendLine($"- **Tags:** {string.Join(", ", analysis.Tags)}");
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("## AI Analysis");
        sb.AppendLine();
        sb.AppendLine("### Summary");
        sb.AppendLine();
        sb.AppendLine(analysis.Summary);
        sb.AppendLine();
        sb.AppendLine("### Key Topics");
        sb.AppendLine();
        sb.AppendLine(analysis.KeyTopics);
        sb.AppendLine();
        sb.AppendLine("### Relevance for Developers");
        sb.AppendLine();
        sb.AppendLine(analysis.Relevance);
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("## Generated LinkedIn Post");
        sb.AppendLine();
        sb.AppendLine("```");
        sb.AppendLine(analyse.LinkedInPost);
        sb.AppendLine("```");
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*");

        File.WriteAllText(filePath, sb.ToString());
        return filePath;
    }

Le constructeur prend un chemin de répertoire de sortie et le crée s’il n’existe pas. La méthode GenerateMarkdownFile prend un objet ArticleAnalysis et produit un document markdown bien formaté.

Le nom du fichier comprend la date et une version épurée du titre. Cela facilite le tri chronologique et l’identification des fichiers en un coup d’œil.

Gestion des noms de fichiers dangereux

Les titres d’articles peuvent contenir des caractères qui ne sont pas autorisés dans les noms de fichiers, comme des points, des barres obliques, des points d’interrogation et des guillemets. Nous devons les désinfecter :

private static string GenerateSafeFileName(string title)
{
    var invalidChars = Path.GetInvalidFileNameChars();
    var safeTitle = new string(title
        .Where(c => !invalidChars.Contains(c))
        .ToArray());
    
    safeTitle = safeTitle.Replace(" ", "-").Replace("--", "-");
    
    if (safeTitle.Length > 50)
    {
        safeTitle = safeTitle.Substring(0, 50);
    }
    
    return safeTitle.TrimEnd('-').ToLowerInvariant();
}

Nous utilisons Path.GetInvalidFileNameChars() pour obtenir une liste de caractères qui ne peuvent pas apparaître dans les noms de fichiers sur le système d’exploitation actuel. Nous les filtrons, remplaçons les espaces par des tirets pour plus de lisibilité, limitons la longueur à 50 caractères et convertissons en minuscules pour plus de cohérence.

Configuration des notifications de télégramme

Pourquoi j’ai choisi Telegram

Pour le composant de notification, j’ai envisagé plusieurs options : e-mail, SMS, Slack, Discord et Telegram. J’ai finalement choisi Telegram pour plusieurs raisons.

L’API est entièrement gratuite, sans limite de débit pour une utilisation raisonnable. De nombreux services de notification imposent des limites au nombre de messages que vous pouvez envoyer gratuitement, mais Telegram ne limite pas les messages de robots à des utilisateurs individuels.

L’API du bot est incroyablement simple. Ce ne sont que des requêtes HTTP avec des charges utiles JSON. Aucun flux d’authentification complexe, aucun webhook requis pour les fonctionnalités de base.Telegram fonctionne partout : sur mon téléphone, sur mon bureau, dans mon navigateur Web. Je peux recevoir des notifications où que je sois et répondre immédiatement.

Les messages prennent en charge un formatage riche. Je peux utiliser du texte en gras, en italique et même des blocs de code pour rendre mes notifications plus lisibles.

Création de votre robot Telegram

Configurer un bot Telegram est étonnamment simple. Ouvrez Telegram et recherchez @BotFather – il s’agit du bot officiel de Telegram pour créer et gérer des robots. Démarrez une conversation avec BotFather et envoyez la commande /newbot. BotFather vous demandera un nom pour votre bot (c’est le nom d’affichage) et un nom d’utilisateur (celui-ci doit être unique et se terminer par « bot »). Une fois que vous les aurez fournis, BotFather créera votre bot et vous donnera un jeton API. Ce jeton est comme un mot de passe : gardez-le secret et ne le confiez pas à des référentiels publics.

Pour trouver votre identifiant de chat afin que le bot sache où envoyer des messages, démarrez une conversation avec votre nouveau bot en le recherchant et en appuyant sur Démarrer. Accédez ensuite à l’URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates dans votre navigateur ou en utilisant curl. Recherchez l’objet chat dans la réponse – le champ id est votre identifiant de chat.

Envoi de messages via l’API

Voici notre fonction pour envoyer des messages Telegram :

static async Task SendToTelegramAsync(string botToken, string chatId, string message)
{
    using var httpClient = new HttpClient();
    
    var telegramApiUrl = $"https://api.telegram.org/bot{botToken}/sendMessage";
    
    var payload = new
    {
        chat_id = chatId,
        text = message,
        parse_mode = "HTML"
    };
    
    var jsonContent = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
    
    var response = await httpClient.PostAsync(telegramApiUrl, content);
    
    if (!response.IsSuccessStatusCode)
    {
        var errorContent = await response.Content.ReadAsStringAsync();
        throw new Exception($"Telegram API error: {response.StatusCode} - {errorContent}");
    }
}

L’API Telegram Bot est basée sur REST. Nous envoyons une requête POST au point de terminaison sendMessage avec un corps JSON contenant l’ID de discussion (où envoyer), le texte du message (quoi envoyer) et éventuellement un mode d’analyse (pour le formatage).

Définir parse_mode sur “HTML” nous permet d’utiliser des balises HTML de base dans nos messages – des éléments comme <b>bold</b> et <i>italic</i>. Cela peut rendre les notifications plus lisibles, même si pour notre cas d’utilisation actuel, nous envoyons du texte brut.

Si la requête échoue, nous générons une exception avec des détails sur ce qui n’a pas fonctionné. Cela aide au débogage si quelque chose ne fonctionne pas.

Configuration de l’application

Variables d’environnement

Notre application a besoin de plusieurs informations sensibles : clés API, jetons de bot et URL de point de terminaison. Nous ne devrions jamais les coder en dur ni les soumettre au contrôle de version. Au lieu de cela, nous utilisons des variables d’environnement, qui peuvent être définies de manière sécurisée dans chaque environnement dans lequel l’application s’exécute.

Pour Telegram, nous avons besoin de TELEGRAM_BOT_TOKEN (le jeton que BotFather vous a donné) et de TELEGRAM_CHAT_ID (votre identifiant de chat où les messages doivent être envoyés).

Pour Azure OpenAI, nous avons besoin de AZURE_OPENAI_ENDPOINT (l’URL de votre ressource), AZURE_OPENAI_API_KEY (votre clé API) et AZURE_OPENAI_DEPLOYMENT (le nom de votre modèle déployé, comme « gpt-4o »).

Chargement de la configuration dans le code

Voici comment nous chargeons ces valeurs au démarrage de l’application :

var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN");
var telegramChatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID");

var azureOpenAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var azureOpenAiDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";

var aiAnalysisEnabled = !string.IsNullOrWhiteSpace(azureOpenAiEndpoint) && 
                        !string.IsNullOrWhiteSpace(azureOpenAiKey);

Nous utilisons Environment.GetEnvironmentVariable pour lire chaque valeur. Pour le nom du déploiement, nous fournissons la valeur par défaut “gpt-4o” si aucune valeur n’est définie.

Nous vérifions ensuite si l’analyse IA doit être activée en vérifiant que nous disposons à la fois d’un point de terminaison et d’une clé API. Cela permet à l’application de s’exécuter en mode dégradé si Azure OpenAI n’est pas configuré : elle récupérera toujours les flux et suivra les articles, juste sans l’analyse de l’IA.### Dégradation gracieuse

Ce concept de dégradation gracieuse est important. Nous ne voulons pas que l’application plante simplement parce qu’une fonctionnalité facultative n’est pas configurée :

ArticleAnalyzer? articleAnalyzer = null;
MarkdownGenerator? markdownGenerator = null;

if (aiAnalysisEnabled)
{
    Console.WriteLine("🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel");
    articleAnalyzer = new ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
    markdownGenerator = new MarkdownGenerator(articlesOutputDir);
}
else
{
    Console.WriteLine("ℹ️  AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable");
}

Si l’IA est activée, nous créons l’analyseur et le générateur de démarques. Sinon, nous les laissons nuls et ignorons les étapes liées à l’IA lors du traitement. L’application offre toujours de la valeur en récupérant des flux et en envoyant des notifications de base, même sans l’amélioration de l’IA.

Automatisation avec les actions GitHub

Pourquoi les actions GitHub

Le véritable pouvoir de cette solution vient de l’automatisation. Nous ne voulons pas exécuter manuellement l’application toutes les quelques heures – nous voulons qu’elle s’exécute automatiquement en arrière-plan.

GitHub Actions est parfait pour cela. Il est intégré à GitHub, il n’y a donc aucun service supplémentaire à configurer. C’est gratuit pour les référentiels publics et comprend de généreuses minutes gratuites pour les référentiels privés. Il peut s’exécuter selon un calendrier, déclenchant notre application à intervalles réguliers. Il dispose d’une gestion intégrée des secrets pour stocker nos clés API en toute sécurité. Et il peut renvoyer les modifications dans le référentiel, gardant ainsi notre fichier de suivi à jour.

Le fichier de workflow

Créez un fichier sur .github/workflows/fetch-and-notify.yml avec le contenu suivant :

name: Fetch DevBlogs and Notify

on:
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  fetch-and-notify:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'
      
      - name: Restore dependencies
        run: dotnet restore src/VsFeedLinkedin.csproj
      
      - name: Build
        run: dotnet build src/VsFeedLinkedin.csproj --no-restore
      
      - name: Run application
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
          AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
          AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
          AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
        run: dotnet run --project src/VsFeedLinkedin.csproj
      
      - name: Commit and push changes
        run: |
          git config user.name "GitHub Actions Bot"
          git config user.email "[email protected]"
          
          if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
            TIMESTAMP=$(date +%Y%m%d_%H%M%S)
            git add posted-articles.md generated-posts/
            git commit -m "chore($TIMESTAMP): processed new articles"
            git push
          else
            echo "No changes to commit"
          fi

Laissez-moi vous expliquer chaque partie. La section on définit le moment où le flux de travail s’exécute. Le déclencheur de planification utilise la syntaxe cron – 0 */6 * * * signifie « à la minute 0 de toutes les 6 heures ». Ainsi, le flux de travail s’exécute à minuit, 6 heures du matin, midi et 18 heures UTC. Le déclencheur workflow_dispatch permet des exécutions manuelles à partir de l’interface utilisateur GitHub, ce qui est utile pour les tests.

Le travail s’exécute sur Ubuntu-latest, qui est une machine virtuelle Linux. Nous vérifions notre référentiel, configurons .NET 9, restaurons les packages NuGet et construisons le projet.

L’étape Exécuter l’application est l’endroit où la magie opère. Nous transmettons nos secrets sous forme de variables d’environnement en utilisant la syntaxe ${{ secrets.SECRET_NAME }}. Ces secrets sont stockés en toute sécurité dans GitHub et ne sont jamais exposés dans les journaux.

Enfin, nous validons toutes les modifications dans le référentiel. Nous configurons Git avec une identité de bot, vérifions s’il y a des modifications dans notre fichier de suivi ou dans le répertoire des publications générées, et si c’est le cas, créons un commit et le poussons.

Configuration des secrets

Pour ajouter des secrets à votre référentiel GitHub, accédez aux Paramètres de votre référentiel, puis Secrets et variables, puis Actions. Cliquez sur “Nouveau secret du référentiel” et ajoutez chacune de vos variables d’environnement. Les noms doivent correspondre exactement à ce que nous référençons dans le fichier de workflow.

Conclusion

Ce que nous avons construit

En repensant à tout ce que nous avons couvert, nous avons créé un agrégateur de flux RSS complet, alimenté par l’IA, qui automatise ce qui était autrefois un processus manuel fastidieux. L’application surveille automatiquement sept flux Microsoft DevBlogs, capturant chaque nouvel article dès sa publication. Il gère les complexités de la déduplication, en reconnaissant le moment où le même article apparaît dans plusieurs flux.L’analyse IA optimisée par Semantic Kernel et Azure OpenAI lit et comprend le contenu des articles, génère des résumés, identifie les sujets clés et explique la pertinence, le tout automatiquement. Plus important encore, il crée des publications LinkedIn attrayantes que je peux partager avec un minimum de modifications.

L’intégration de Telegram signifie que je suis averti sur mon téléphone chaque fois qu’il y a un nouveau contenu à consulter. Je peux consulter le message, décider si je souhaite le partager et agir immédiatement.

Et comme il s’exécute sur GitHub Actions selon un calendrier, je n’ai pas besoin de me rappeler de faire quoi que ce soit. Le système fonctionne en arrière-plan et je ne m’engage que lorsqu’il y a quelque chose qui mérite d’être partagé.

Les technologies qui ont rendu cela possible

Ce projet a regroupé plusieurs technologies qui ont chacune joué un rôle crucial. .NET 9 a fourni une base solide avec ses fonctionnalités de langage moderne et ses excellentes performances. Semantic Kernel a simplifié l’intégration de l’IA, en gérant toute la complexité des appels d’API et de la gestion des réponses. Azure OpenAI a fourni l’intelligence : la capacité de réellement comprendre et analyser le contenu technique. HtmlAgilityPack a résolu le problème compliqué de l’extraction de texte propre à partir de pages Web. System.ServiceModel.Syndication a facilité l’analyse RSS. L’API Telegram Bot nous a fourni des notifications gratuites et fiables. Et GitHub Actions a tout lié avec une exécution automatisée et planifiée.

Penser aux coûts

Une question que vous pourriez vous poser est la suivante : combien cela coûte-t-il de fonctionner ? La réponse est : pas grand-chose du tout.

Telegram est entièrement gratuit – aucun frais pour l’envoi de messages via votre bot.

GitHub Actions est gratuit pour les référentiels publics. Pour les référentiels privés, vous bénéficiez de 2 000 minutes par mois sur l’offre gratuite, ce qui est plus que suffisant pour notre cas d’utilisation.

Azure OpenAI est le seul composant payant et les coûts sont minimes. Avec GPT-4o, l’analyse d’un article de blog typique coûte entre un et trois centimes. Même si vous traitez des dizaines d’articles par mois, les coûts de l’IA sont inférieurs à un dollar.

Où vous pourriez prendre ceci ensuite

Bien que cette solution réponde parfaitement à mes besoins, il existe de nombreuses façons de l’étendre. Vous pouvez ajouter la prise en charge de plusieurs plateformes sociales – peut-être publier sur Twitter/X, Mastodon ou Bluesky en plus de LinkedIn. Vous pouvez mettre en œuvre une analyse des sentiments pour suivre le ton des articles au fil du temps et repérer les tendances. Vous pouvez autoriser différents modèles d’invites pour différents flux, générant ainsi différents styles de publications pour différents sujets. Vous pouvez créer un tableau de bord Web pour examiner et gérer les publications au lieu d’utiliser Telegram. Vous pouvez suivre les mesures d’engagement pour le contenu publié afin de voir quels sujets trouvent le plus d’écho auprès de votre public.

Réflexions finalesCe que j’aime le plus dans ce projet, c’est qu’il incarne une philosophie à laquelle je crois fermement : l’automatisation doit gérer les parties fastidieuses tout en laissant les parties créatives et décisionnelles aux humains. Le système fait tout le gros du travail – récupérer, analyser, analyser, générer – mais je vérifie toujours tout avant de partager. Les publications générées par l’IA sont des points de départ que je peux personnaliser et personnaliser.

En combinant la puissance de .NET, Semantic Kernel et Azure OpenAI, nous avons créé un outil qui permet d’économiser des heures de travail manuel chaque semaine tout en maintenant la qualité et la cohérence. C’est le genre d’automatisation pratique qui fait une réelle différence dans la vie quotidienne.

Si vous construisez quelque chose de similaire ou si vous avez des idées d’améliorations, j’aimerais en entendre parler. N’hésitez pas à nous contacter sur LinkedIn !

Bon codage et joyeux Noël ! 🎄