Créer un bot Discord avec Haskell

Voir aussi : video youtube - video peertube - code source

Discord est un système de messagerie instantanée et de VoIP très connu. Il propose une API permettant de coder des bots, c’est-à-dire des programmes informatiques qui interagissent avec le système et ses utilisateurs. Un bot peut, par exemple, muter tous les participants dans un salon audio, répondre automatiquement à des messages textes, dialoguer avec un humain grâce à une IA (chatbot)…

Cet article explique comment créer un bot Discord codé en Haskell, de la mise en place sur un serveur Discord jusqu’à l’implémentation d’une interface vers un programme Hoogle (le moteur de recherche de fonctions Haskell). Il s’inspire en partie du tutoriel Créer un Bot Discord avec Python.

Création d’un bot sur Discord

Concrètement, un bot est un programme qui tourne en permanence sur une machine quelconque (serveur, ordinateur personnel, etc) et qui communique avec un serveur Discord via l’API.

Avant de lancer ce programme, il faut donc créer un serveur Discord ou être administrateur sur un serveur Discord existant. Pour ajouter un bot, il faut alors :

Cette URL est de la forme https://discordapp.com/oauth2/authorize?client_id=...&scope=bot&permissions=.... Elle permet d’ajouter le bot que l’on vient de créer à notre serveur Discord. Pour cela, il suffit d’accéder à l’URL avec le navigateur web puis d’indiquer le serveur Discord qui doit accueillir le bot.

On peut désormais exécuter un programme de Bot, qui interagira alors avec ce serveur. Pour cela, il faut également récupérer le token d’accès sur le Developer Portal (onglet Bot) et, par exemple, l’écrire dans un fichier token.txt sur la machine exécutant le programme de Bot.

Écriture d’un bot “d’écoute”

Haskell propose différentes bibliothèques pour coder des bots Discord. Ici, on utilisera discord-haskell. Avec Nix, on peut écrire le fichier shell.nix suivant, qui récupère le code source de la bibliothèque et crée un environnement suffisant.

let

  discord-haskell-src = fetchTarball {
    url = https://github.com/aquarial/discord-haskell/archive/8e1988e.tar.gz;
    sha256 = sha256:1m074p7xi016qg6wv08zqzz4jhywgm65k1amk2hqji0xm1wr1d76;
  };

  config = {
    allowBroken = true;
    packageOverrides = pkgs: rec {
      haskellPackages = pkgs.haskellPackages.override {
        overrides = self: super: rec {
          discord-haskell = self.callCabal2nix "discord-haskell" discord-haskell-src {};
        };
      };
    };
  };

  pkgs = import <nixpkgs> { inherit config; };

in with pkgs; mkShell {
  buildInputs = [
    (haskellPackages.ghcWithHoogle (ps: with ps; [
      discord-haskell
    ]))
  ];

  shellHook = ''
    export PATH="$PATH:${pkgs.haskellPackages.hoogle}/bin"
  '';
}

On peut alors exécuter un fichier de code Haskell très simplement, avec nix-shell et runghc. Par exemple, le fichier listen.hs suivant :

import Control.Monad (when, void, unless)
import Data.List (isPrefixOf)
import Data.Text (pack, unpack)
import System.Process (readProcess)
import UnliftIO (liftIO)

import qualified Discord as D
import qualified Discord.Types as D
import qualified Discord.Requests as D

main :: IO ()
main = do
    putStrLn "running..."
    token <- pack <$> readFile "token.txt"
    let opts = D.def { D.discordToken = token, D.discordOnEvent = eventHandler }
    D.runDiscord opts >>= print

eventHandler :: D.Event -> D.DiscordHandler ()
eventHandler (D.MessageCreate m) = liftIO $ print m     -- affiche les messages reçus
eventHandler _ = pure ()

Ce premier bot est très simple : on lance une application Discord avec le token lu dans le fichier token.txt et le gestionnaire d’événements eventHandler. Ce gestionnaire récupère tous les messages de notre serveur Discord et les affiche sur le terminal de la machine exécutant le bot.

Pour exécuter le bot, on peut lancer la commande suivante :

$ nix-shell --run "runghc listen.hs"
running...

Le bot écoute alors les événements sur le serveur Discord, et si quelqu’un envoit un message :

alors le bot reçoit l’événement et l’affiche sur son terminal local :

Message {messageId = 793422045328179233, ... messageText = "hello from discord" ...

Écriture d’un bot “echo”

Ainsi, la bibliothèque discord-haskell est une classique interface vers l’API originelle, avec un système d’événements (voir la documentation et les exemples).

Pour implémenter un bot d’echo (qui répète chaque message envoyé sur le serveur Discord), on vérifie que l’auteur du message n’est pas un bot, on récupère le texte du message et son canal, et on envoie un nouveau message avec ce même texte et sur ce même canal.

eventHandler :: D.Event -> D.DiscordHandler ()
eventHandler (D.MessageCreate m) = 
    unless (D.userIsBot $ D.messageAuthor m) $ do       -- si le message ne vient pas d'un bot
        let txt = D.messageText m                       -- récupère le texte du message
            chan = D.messageChannel m                   -- et son canal
        void $ D.restCall (D.CreateMessage chan txt)    -- et envoie un nouveau message
eventHandler _ = pure ()

Écriture d’un bot “hoogle”

Pour terminer, voyons un bot un peu plus interessant, qui interroge le programme hoogle. Hoogle est un moteur permettant de recherche des fonctions Haskell d’après leur nom ou leur signature. Il est disponible en ligne mais également en local :

$ hoogle --count=5 putStrLn
Distribution.Compat.Prelude.Internal putStrLn :: String -> IO ()
Prelude putStrLn :: String -> IO ()
System.IO putStrLn :: String -> IO ()
Data.ByteString putStrLn :: ByteString -> IO ()
Data.ByteString.Char8 putStrLn :: ByteString -> IO ()
-- plus more results not shown, pass --count=15 to see more

On peut donc implémenter un bot qui écoute les messages sur le serveur Discord, regarde si leur contenu commence par le préfix “hoogle” et, le cas échéant, lance hoogle localement avec la suite du message puis envoie le résultat sur le serveur Discord.

eventHandler :: D.Event -> D.DiscordHandler ()
eventHandler (D.MessageCreate m) = do
    let str = unpack $ D.messageText m  -- récupère le texte du message
        hoogleReq = checkHoogle str     -- requête hoogle ?
        chan = D.messageChannel m
    when (hoogleReq /= "") $ do
        -- exécute hoogle
        hoogleRes <- liftIO $ readProcess "hoogle" ["--count=5", hoogleReq] ""
        -- récupère et formate le résultat
        let txt = pack $ "```hs\n" <> hoogleRes <> "\n```"
        -- envoie le résultat sur Discord
        void $ D.restCall (D.CreateMessage chan txt)
eventHandler _ = pure ()

-- Vérifie si c'est une requête pour hoogle. Retourne la requête ou une chaine vide.
checkHoogle :: String -> String
checkHoogle str = if "hoogle " `isPrefixOf ` str then drop 7 str else ""

Conclusion

Grâce à son API, Discord permet d’écrire des bots, c’est-à-dire des programmes qui interagissent avec un serveur Discord, par exemple pour répondre automatiquement à des messages, fournir un service particulier, générer des statisques d’utilisation, etc.

Dans cet article, on a vu comment écrire quelques bots simples en Haskell, avec la bibliothèque discord-haskell. Cette dernière est basée sur un gestionnaire d’événements auquel on fournit des fonctions de rappel. Ce système est très naturel dans un langage fonctionnel comme Haskell, d’où la simplicité des codes sources présentés ici.

Il existe également d’autres bibliothèques Haskell pour écrire des bots Discord, notamment calamity. Cette dernière utilise des notions plus avancées de Haskell mais est plus évoluée. Elle propose notamment un DSL pour définir les commandes d’un bot, ainsi qu’un gestionnaire d’effets pour préciser les fonctionnalités du bot (cache, métriques, logs…).