Projet de mini chat bot écrit en TypeScript, pour apprendre à coder en TS avec exemple détaillé de déclarations et écriture de foncions simples

MiniChatBot : Un Chatbot Simple (sans IA) et détaillé, pour apprendre TypeScript

Bienvenue dans le monde de MiniChatBot, un petit projet TypeScript ludique, pour vous montrer comment coder proprement nos variables et fonctions TS, avec des typages forts ! Ce mini chatbot est en fait une interface de dialogue entre l'ordinateur et soi-même, basée sur des règles simples et personnalisables. Bien sûr, il ne s'agit pas là d'une IA au sens moderne du terme (où il y aurait de l'apprentissage, par exemple), mais simplement d'un compagnon textuel amusant qui répondra à vos saisies au clavier (avec des réponses variées, empreint d'humour quelquefois !).

Dans cet article, je vais tout d'abord vous expliquer pourquoi j’ai créé ce projet, puis vous détailler chaque partie du code. Le but étant, comme d'habitude ici, d'apprendre à coder en TS par l'exemple, tout en s'amusant ! Intéressé ? Alors allons-y !

Remarque : ce mini projet de chat bot est fait pour vous inspirer avant tout, tout en vous montrant tout un tas de bonnes pratiques en TS. Et comme c'est un projet à but didactique, vous trouverez bien évidemment bon nombre de variables avec des noms à rallonge (pour que ça soit le plus parlant possible !), et tout un tas de commentaires au sein du code (à destination de ceux qui débutent en TypeScript, bien entendu). Du reste, si vous repérez la moindre coquille ou avez des questions au sujet de ce programme MiniChatBot, n'hésitez pas à le faire savoir en commentaire ! Merci 😉

Salut ! Bienvenue sur LeCoinTS, où je partage des tutos TypeScript gratuits et sans pub. Envie de me soutenir dans cette aventure, avec un café ? ☕

Offrez moi un café LeCoinTS

Pourquoi ce projet ?

MiniChatBot est né d’une envie simple : démontrer qu’on peut coder un mini programme interactif, à la fois simple et robuste, en TypeScript ! Et que vous soyez débutant ou un simple développeur curieux, c'est le genre de projet idéal pour apprendre TS par l'exemple ! D'ailleurs, voici en détail les buts visés par ce projet :

  • Apprendre TypeScript : avec des typages explicites et une syntaxe claire, ce projet est un bel exemple de codage propre, et robuste
  • Apprendre à stocker des données dans un fichier JSON : les règles, définissant les réponses possibles du chatbot en fonction de ce que vous aurez saisi au clavier, sont stockées dans un fichier JSON externe ; ici, il s'agit de montrer comment les récupérer, en gérant les éventuelles erreurs qui pourraient se produire au passage
  • Ajouter du fun en plus de la pédagogie : avec une invite personnalisée où le ChatBot se présentera à vous, et des blagues intégrées, vous verrez ici comment donner un semblant de personnalité à un programme !

Au passage, ne croyez pas que ce projet est quelque chose de limité ou sans avenir. Car grâce à ses règles personnalisables (panel de réponses possibles pour chaque mot clef ou terme identifié dans les entrées clavier de l'utilisateur), ce bot n'a de limite que la quantité de cas qu'on aura anticipé, et décrit dans les règles ! C'est d'ailleurs ce qui fait que ce mini bot est rigolo et peut sembler "intelligent" à bien des égards, tout en restant à la ramasse le reste du temps !

Cela étant dit, vous verrez également que ce MiniChatBot n'en reste pas moins attachant pour autant ! Car avec suffisamment de règles (en plus des quelques règles de base que j'ai renseigné ici), vous pourriez obtenir quelque chose de vraiment surprenant ! D'ailleurs : essayez-le, et vous verrez ! Au bout d'un moment, on se prend au jeu 🙂

Un aperçu du Mini Chat Bot en action, sous VS Code !

Vous vous demandez peut-être à quoi ressemble ce chat bot ! Alors, avant de vous montrer le code, voici un exemple de questions que je lui ai soumis depuis le terminal VSCode, et les réponses que le ChatBot m'a fait en retour !

Aperçu questions réponses d'un mini chat bot TypeScript, pour apprendre à coder en TS proprement avec interaction utilisateur au clavier

Bon… là je n'avais mis que des règles simples, et posé des questions en conséquence ! Mais franchement, y'a du potentiel 😉

Maintenant, voyons comment vous pouvez télécharger et installer ce chat bot, afin que vous puissiez l'essayer, et comprendre comment il fonctionne en interne !

Remarque : au passage, vous avez là un aperçu de l'organisation des fichiers du projet, sur la gauche de l'image. Les fichiers sources TS (non exécutables) ont été mis dans le répertoire src (source). Les fichiers compilés JS (exécutables), issus des fichiers TS, seront mis dans le répertoire dist (distribution). Du reste, on trouve tout un tas de fichiers à la racine, comme les fichiers de configuration pour le compilateur tsc (tsconfig.json), pour l'analyseur de code ESLint (eslint.config.mjs), et du formateur de code Prettier (.prettierrc).

Téléchargement et installation

Alors, pour installer ce Mini Chat Bot à la sauce TypeScript, il faut s'assurer de plusieurs choses avant (ce qu'on appelle les "prérequis"). Nous allons donc les voir à présent, avant de passer à la partie téléchargement / installation, à proprement parler.

Prérequis

Pour installer le MiniChatBot sur votre ordi, vous aurez besoin des éléments suivants :

  • avoir NodeJS et npm installés
  • avoir git installé (afin de pouvoir récupérer tous les fichiers de ce bot)
  • et, optionnellement, avoir Visual Studio Code installé (si vous souhaitez avec un environnement de test et de développement sympathique !)

Téléchargement / installation

Alors, pour installer ce projet, il faudra tout d'abord vous placer là où vous souhaitez que le répertoire du MiniChatBot soit créé ; puis ouvrir un terminal à ce niveau. Une fois fait, il faudra saisir les commandes suivantes, dans le terminal :

  • git clone https://github.com/LeCoinTS/MiniChatBot.git pour télécharger le projet
  • cd MiniChatBot pour entrer dans le répertoire projet
  • npm i pour installer les dépendances du programme

Si tout s'est bien déroulé, il vous restera plus qu'à taper npm run start dans votre terminal, pour lancer le programme ! Voici ce à quoi ça ressemble, si je fais ça sur le terminal Windows (via le terminal cmd) :

Téléchargement et installation mini chat bot typescript depuis terminal Windows cmd, pour apprendre TS à travers un projet ludique pour débutant

Comment ça marche ? Analyse du code morceau par morceau

Ici, je vais vous détailler comment fonctionne ce bot, bloc après bloc, pour que ce soit le plus digeste possible (c'est toujours mieux qu'un gros pavé, hein !!). Alors commençons tout de suite avec les imports et variables globales, sans plus attendre !

Imports et constantes globales

Voici à quoi ressemblent les premières lignes de code de ce bot (qui tient sur un seul et même fichier, pour info) :

import * as path from "path";
import * as fs from "fs/promises";
import * as readline from "readline";

const cheminVersFichierJSON: string = path.join(process.cwd(), "src");
const nomDuFichierJSON: string = "regles.json";

On importe ici tous les modules nécessaires à la bonne marche du bot, à savoir :

  • path pour gérer les chemins sur le disque dur (pour retrouver nos données JSON, qui contiennent les règles régissant les réponses possibles du bot)
  • fs/promises pour lire des fichiers sur le disque dur (plus précisément, lire le fichier JSON des règles) ; à noter qu'on importe la "version" avec "promises" dans notre cas, du fait qu'on va lire notre fichier JSON de manière asynchrone (structure async/await, dans le code)
  • et readline pour gérer l’interaction avec le clavier utilisateur

Les constantes, quant à elles, définissent le chemin menant au fichier regles.json, où seront stockées les données. À noter que j'ai utilisé l'instruction process.cwd() ici, afin d'obtenir le répertoire de lancement du script, fournissant un chemin absolu fiable (le script étant lancé depuis la racine du projet, via la commande npm run start, pour rappel).

Interface et initialisation

Arrivent ensuite les interfaces/initialisations de ce mini chat bot :

interface Regle {
  pattern: string | RegExp;
  responses: string[];
  isRegex: boolean;
}

const userInterface: readline.Interface = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const regles: Regle[] = [];

Ici, on adopte un typage fort, c'est à dire qu'on va expliciter tous les types des variables/interfaces/fonctions qu'on va utiliser. Ainsi :

  • l’interface Regle définit la structure des règles (sachant que chaque règle contiendra une pattern, une ou plusieurs réponses possibles pour le bot, et un type de matching)
  • le userInterface est ce sur quoi reposera toute l'interaction bot <-> utilisateur (via les saisies au clavier, dans la console)
  • et regles est un tableau qui contiendra toutes les règles contenues dans le fichier regles.json (afin de ne pas avoir à lire et relire sans arrêt ce fichier JSON !) ; à noter que ce tableau de règles est vide au démarrage, mais bien évidemment rempli par la suite 😉

Remarque technique : malgré son nom, userInterface n'est pas une interface TypeScript, à proprement parler. Il s'agit en fait ici d'une instance d'objet, de type readline.Interface, retourné par readline.createInterface(...). Cet objet (héritant de readline > EventEmitter en interne) disposera, pour info, des méthodes on, prompt, et close, qui nous sera grandement utile par la suite (pour gérer l'interaction de l'utilisateur avec son clavier/écran).

Vous aimez cet article ? LeCoinTS reste gratuit et sans pub grâce à vos dons.
Motivez-moi à en faire plus ! ☕

Offrez moi un café LeCoinTS

Message d’accueil

Puis vient le message d'accueil, qui s'affiche au lancement du chatbot :

const messageAccueil: string = `
*****************************************************************************************************************
Salut ! Je suis un MiniChatBot, créé par un développeur un peu fou, en 2025 ! À l’origine, j’étais juste
un bout de code pour répondre à des 'bonjour', mais j’ai grandi en rêvant de devenir un compagnon utile ;)
J’aime bien discuter, faire sourire les gens, mais suis quelque peu maladroit avec les questions compliquées... !
Après tout, je ne suis encore qu'un bébé, dans le monde des bots ;)
*****************************************************************************************************************`;

Pour info, ce type de message d'accueil s'appelle une "backstory", qui dans notre cas, donne une personnalité au bot. Comme vous vous en doutez, tout est fictif ici ; et c'est simplement pour décrire la vie du bot (d'où il vient et qui il est), que je l'ai mis en place. Avouez que ça donne un petit côté sympathique au programme, hein !

Chargement des règles

Bon à présent, entrons un peu plus dans le code, et voyons la première "grosse" fonction, permettant ici de charger toutes les règles du bot (qui sont, pour rappel, stockées dans un fichier externe au programme, au format JSON) :

const chargerRegles = async (): Promise<void | never> => {
  // Vidange du tableau des règles (car en cas de reload, ce tableau contien déjà les anciennes valeurs, qu'il faut virer)
  regles.splice(0, regles.length)

  // Chargement des règles...
  try {
    const fichierJSONaLire: string = path.join(cheminVersFichierJSON, nomDuFichierJSON)
    const donneesJSONauFormatTexte: string = await fs.readFile(fichierJSONaLire, "utf8")
    const donneesJSONmisDansUnTableauDeRegle: Regle[] = JSON.parse(donneesJSONauFormatTexte)

    donneesJSONmisDansUnTableauDeRegle.forEach((regle) => {
      // On stocke chaque règles une à une dans notre tableau, en créant une regex au besoin au niveau du pattern
      regles.push({
        ...regle,
        pattern: regle.isRegex ? new RegExp(regle.pattern, "i") : regle.pattern,
      })
    })
    // Règles chargées, on sort !
  } catch (error) {
    console.error("\n[ERREUR] Impossible de charger les règles, désolé\n")
    console.error("Erreur détaillée :", error)
    userInterface.close()
    process.exit(1)
  }
}

Grosso modo, avec cette fonction, nous allons charger l'ensemble des règles (réponses potentielles du bot en fonction des questions utilisateur) qui sont stockées dans le fichier JSON. À noter que cette fonction est appelée au lancement du programme, mais également lorsque l'utilisateur tape "reload" au clavier (ce qui permet de recharger les règles JSON, si elles sont été changées en cours de route, sans avoir à arrêter/relancer le programme). Du reste, ces règles seront chargées dans une variable "globale", nommée regles.

Alors, dans le détail, nous commençons par vider le tableau de règles (nota : ce tableau est forcément vide au démarrage, mais pas forcément après ; d'où la nécessité de le purger, dans l'éventualité d'un reload demandé par l'utilisateur). Cette vidange se fait avec l'instruction splice dans notre cas, qui permet de retirer les éléments d'un tableau, d'un index donné à un autre (ici : de l'index 0 jusqu'au dernier index, pour tout supprimer). À noter qu'on aurait également pu vider ce tableau en écrivant simplement regles.length = 0 ; car oui, length s'utilise dans 2 sens : en lecture pour connaître le nombre d'éléments d'un tableau, et en écriture si on le souhaite, pour forcer la "longueur" d'un tableau (en mettant cette dernière à zéro, on ferait disparaître tous les éléments contenus dans le tableau d'un seul coup).

Ensuite, on lit les données contenues dans le fichier JSON (avec la commande fs.readFile), puis on stocke toutes ces données dans un tableau temporaire nommé donneesJSONmisDansUnTableauDeRegle. Une fois fait, on parcourt ligne à ligne ce tableau pour analyser si les patterns sont de type RegExp ou non, et stocker au fur et à mesure ces données en conséquence, dans le tableau final (nommé regles, pour rappel).

Remarque 1 : nous n'avons pas chargé directement les données JSON dans le tableau de règles, car il fallait justement adapter les propriétés pattern, selon si elles étaient au format texte, ou au format regex ("expression régulière", en français).

Remarque 2 : vous aurez peut-être du mal à comprendre la ligne regles.push({…regle, pattern: regle.isRegex ? new RegExp(regle.pattern, "i") : regle.pattern}). En fait, ce qui est présent entre les parenthèses du push() s'appelle de la déstructuration. Ici, nous faisons cela pour "éclater" chaque règle pour en extraire toutes leurs propriétés (pour rappel, l'interface Regle vue plus haut a défini qu'une règle contenait 3 propriétés : pattern, responses, et isRegex). Ainsi :
- la partie ...regle permet de récupérer toutes les propriétés de la règle
- et la partie pattern: regle.isRegex ? new RegExp(regle.pattern, "i") : regle.pattern permet de "réécrire" la propriété pattern, avec une nouvelle valeur (en l'occurrence, ici, une valeur conditionnelle, selon si la pattern était au format string, ou au format expression régulière, c'est à dire une regex)

Récupération d’une réponse

Maintenant que toutes nos règles (celles qui définissent quelles réponses peuvent être retournées en fonction de ce qui a été entré par l'utilisateur) sont écrites, je vous propose de voir à présent la fonction qui va piocher dans ces règles, selon le texte saisi par l'utilisateur.

Tout d'abord, jetons un coup d'œil à cette partie du code :

const recupereUneReponse = (texteSaisiParUtilisateur: string): string => {
  // On met le texte entré en minuscule, pour ne pas être sensible à la casse
  const texteSaisiMisEnMinuscule: string = texteSaisiParUtilisateur.toLowerCase()

  // Parcours de toutes les règles, pour voir s'il y en a une qui match avec ce qu'a saisi l'utilisateur
  for (const regle of regles) {
    // En excluant le cas particulier de la "réponse par défaut"
    if (typeof regle.pattern === "string" && regle.pattern === "default") {
      continue
    }
    if (regle.pattern instanceof RegExp && regle.pattern.test(texteSaisiMisEnMinuscule)) {
      // Si la pattern est de type RegExp, on utilise un "pattern.test"
      return regle.responses[Math.floor(Math.random() * regle.responses.length)] // Choix aléatoire
    } else if (typeof regle.pattern === "string" && texteSaisiMisEnMinuscule.includes(regle.pattern)) {
      // Si la pattern est de type string, on utilise un "texte.includes"
      return regle.responses[Math.floor(Math.random() * regle.responses.length)] // Choix aléatoire
    }
  }

  // Si aucune règle ne correspond au texte saisi, on retourne une réponse par défaut
  const defaultRegle: Regle | undefined = regles.find((regle) => regle.pattern === "default")
  return defaultRegle ? defaultRegle.responses[Math.floor(Math.random() * defaultRegle.responses.length)] : "Je suis perdu... !"
}

Cette fonction démarre avec la mise en minuscule du texte entré par l'utilisateur, pour commencer. Pourquoi ? Tout simplement pour ne pas être "sensible à la casse" (c'est à dire, ne pas avoir à se soucier des minuscules ou majuscules). Cela permet d'interpréter de la même manière un "ça va ?" ou "Ça va ?", par exemple, ce qui nous simplifiera la vie, dans notre mini chat bot !

Ensuite, on effectue un balayage de toutes les règles en mémoire, pour voir si l'une d'elles pourrait correspondre (matcher) avec le texte saisi par l'utilisateur. À noter que ce balayage est divisé en deux, car :

  • si la règle à vérifier a un pattern de type regex (expression régulière), alors on teste si ce regex match avec ce qu'a entré l'utilisateur
  • si la règle à vérifier a un pattern de type string (chaîne de caractère), alors on teste si cette chaîne de caractère est incluse dans ce qu'a entré l'utilisateur

S'il y a correspondance, dans l'un ou l'autre cas, alors on retourne aléatoirement l'une des réponses possibles. Cela se fait en générant un nombre aléatoire entre 0 (compris) et 1 (exclus) grâce à Math.random(), puis en multipliant ce nombre par le nombre de réponses possibles (donné par responses.length) ; une fois fait, il ne reste plus qu'à appliquer un arrondi à l'entier inférieur (grâce à Math.floor()) afin de pouvoir sélectionner la réponse aléatoire correspondante (entre la première règle à l'index 0 inclus, et la n-ième règle, à l'index n-1 inclus).

Enfin, s'il n'y a aucune correspondance trouvé dans les règles, vis à vis de ce qu'a saisi l'utilisateur, alors on retourne une réponse par défaut (ou plutôt une réponse aléatoire parmi toutes celles possibles, comme détaillé précédemment).

En résumé : la fonction recupereUneReponse compare l’entrée utilisateur (mis en minuscules) aux patterns inscrits en minuscule dans les règles. Si une règle correspond (via un test pour les regex, ou un includes pour les string), alors cette fonction renvoie une réponse aléatoire, parmi toutes celles possibles (stockées dans la propriété responses) ; sinon, elle utilise la règle "default", et renvoie une des réponses possibles par défaut.

Fonction principale

Et on arrive enfin à la fonction principale, qui va gérer l'interface utilisateur, et appeler la fonction précédente après chaque saisie validée par l'utilisateur. Sans plus attendre, voyons le code de cette partie, que je vais vous expliquer en détail en suivant :

const main = async (): Promise<void> => {
  console.log(messageAccueil)

  // Chargement des règles
  await chargerRegles()

  // Mise en place d'une écoute au niveau du clavier
  console.log("\nTape quelque chose (ou 'exit' pour quitter), et je te répondrais !\n")

  userInterface
    // Cas où l'utilisateur aurait saisi quelque chose, et appuyé sur ENTREE ensuite
    .on("line", async (texteSaisiParUtilisateur) => {
      if (texteSaisiParUtilisateur.toLowerCase() === "exit") {
        // Si "exit" est tapé au clavier, alors on quitte l'app
        console.log("MiniChatBot : À la prochaine !")
        userInterface.close()
        return
      } else if (texteSaisiParUtilisateur.toLowerCase() === "reload") {
        // Si "reload" est tapé au clavier, alors on recharge les règles contenues dans le fichier JSON
        await chargerRegles()
        console.log("MiniChatBot : Règles rechargées !")
        userInterface.prompt() // Pour réafficher le ">" d'invite à saisir du texte, au niveau de la console
      } else {
        // Sinon, on récupère une des réponses possibles en fonction du texte saisi par l'utilisateur
        const responseDuChatbot: string = recupereUneReponse(texteSaisiParUtilisateur)
        console.log(`MiniChatBot : ${responseDuChatbot}`)

        // Et on réaffiche le ">" après la réponse du bot, pour indiquer
        // à l'utilisateur qu'il peut à nouveau taper une nouvelle question
        userInterface.prompt()
      }
    })
    // Cas où il est demandé à l'interface utilisateur de se fermer
    .on("close", () => {
      console.log("\nChat terminé.")
    })

  // Affichage d'un ">" initial à l'écran, signifiant l'attention d'une première saisie par l'utilisateur
  userInterface.prompt()

  // Nota : ce programme ne s'arrête pas, tant que userInterface.close() n'est pas appelé (ou CTRL+C tapé au clavier !)
}

// Lancement de l'app ici
main()

Ici, tout se passe autour de l'interface utilisateur nommée userInterface (que nous avons instancié tout au début). Cette "interface" (pas au sens TypeScript du terme, mais au sens interaction utilisateur), hérite des propriétés/fonctions d'EventEmitter en interne ; elle dispose donc notamment d'une fonction on, qui s'exécute à l'occasion d'évènements particuliers. On voit d'ailleurs plusieurs "on" dans l'extrait de code ci-dessus ; ces "on" là permettent de détecter plusieurs choses, comme notamment :

  • on("line", ...) pour exécuter un code particulier à chaque nouvelle line (c'est à dire, à chaque fois que l'utilisateur aura saisi du texte, et validé avec la touche ENTRÉE)
  • on("close", ...) pour exécuter un code particulier lorsqu'on clôturera (close) l'interface utilisateur

Par ailleurs, l'interface utilisateur userInteface dispose de fonctions natives (issues de readline.createInterface), que sont :

  • userInterface.prompt() : pour faire afficher le symbole ">" en début de ligne (à rappeler à chaque nouvelle ligne), invitant l'utilisateur à entrer du texte (ce qui rend l'interface plus claire pour celui qui s'en sert ; sinon, on ne saurait pas vraiment quand est-ce qu'on est invité à taper du texte !)
  • userInterface.close() : pour fermer l'interface utilisateur, et ainsi, mettre fin au programme (car, pour info : tant que cette commande n'aura pas été lancée, notre programme restera toujours en attente d'une entrée utilisateur). Du reste, l'appel de cette instruction déclenchera automatiquement l'appel de la commande on("close", ...) vu juste avant, comme vous l'aurez certainement déjà compris

En résumé, la fonction main ne fait donc qu'inviter l'utilisateur à entrer quelque chose au clavier, après avoir mis un "écouteur" à ce niveau et défini qu'est-ce qui doit se passer dans tel ou tel cas (comme avec on("line", ...)). Ainsi, à chaque nouvelle entrée utilisateur (chaque nouvelle line, donc), notre programme appelle la fonction recupereUneReponse() pour savoir quoi répondre à l'utilisateur. Tout en sachant que si l'utilisateur a entré le mot "exit" ou "reload", alors le programme fera un traitement particulier à la place (respectivement quitter le programme en fermant l'interface utilisateur, ou recharger les règles contenues dans le fichier JSON).

Conclusion

Voilà ! Nous voici au terme de ce mini projet de chat bot ! J'espère que mes explications auront été suffisamment claires ! Sinon, n'hésitez pas à poser vos questions en commentaire, en dessous, car c'est fait pour ça !

Par ailleurs, n'hésitez pas à vous inspirer de cet exemple de code TypeScript, pour vos futurs projets ! Car c'est également fait pour ça ! Et si ce genre de projet pratique et concret vous plaît, n'hésitez pas à me motiver à en faire plus, en m'offrant un petit café ! (cf. bouton jaune ci-dessous). Ainsi, ça soutiendra le site LeCoinTS, afin qu'il reste 100% gratuit et SANS PUB aucune !

Merci à vous, par avance !
Jérôme.

Merci d’avoir lu cet article ! LeCoinTS reste gratuit et sans pub grâce à vous.
Un petit don pour soutenir le site ? ☕

Offrez moi un café LeCoinTS
Site LeCoinTS.fr

JEROME

Passionné par tout ce qui touche à la programmation informatique en TypeScript, sans toutefois en être expert, j'ai à coeur de vous partager ici, peu à peu, tout ce que j'ai appris, découvert, réalisé, et testé jusqu'à présent ! En espérant que tout cela puisse vous servir, ainsi qu'au plus grand nombre de francophones possible !

(*) Mis à jour le 14/04/2025

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Résolvez cette soustraction :

67 - ____ = 59

Soutenir LeCoinTS

×

Soutenez le site LeCoinTS en offrant un café symbolique, afin que celui-ci reste 100% gratuit et sans pub, et pour me motiver à faire d'autres articles !
1 café = 5 € environ
Plusieurs cafés = au top !!!

→ Via Logo PayPal
Soutenir avec PayPal
→ Via Logo Stripe
Soutenir avec Stripe
Merci à vous ❤️