DEV Community

Cover image for Créer un bot Telegram avec Elixir
jean-smaug
jean-smaug

Posted on • Originally published at maximeblanc.fr

Créer un bot Telegram avec Elixir

En ces périodes de confinement, il est important de continuer de générer du lead. C'est pour cela que nous allons voir comment créer un bot Telegram pour nous assister dans nos taches du quotidien ne rien faire de productif.

Les sources sont disponibles ici.

Initialisation

Côté Telegram

On commence par s'adresser au daron de tous les bots, j'ai nommé @botfather. On lui demande de créer un nouveau bot avec la commande /newbot, puis en répondant aux questions.

create-bot

Une fois que le bot est créé, botfather nous affiche un token. Nous nous en servirons par la suite.

Côté code

On initialise un projet Elixir avec la commande suivante.

mix new macron_bot --sup

Nous allons utiliser la librairie ex_gram. Cette librairie est une couche d'abstraction autour des API Telegram.

Il faut ajouter cette dépendance ainsi que d'autres qui sont requises pour son bon fonctionnement.

# mix.exs

defmodule MacronBot.MixProject do
  # ...

  defp deps do
    [
      {:ex_gram, "~> 0.12"},
      {:tesla, "~> 1.2"},
      {:hackney, "~> 1.12"},
      {:jason, ">= 1.0.0"}
    ]
  end
end

On installe les dépendances.

mix deps.get

Nous allons ensuite créer un fichier de configuration. (Qui n'est plus généré automatiquement depuis la version 1.9.0 d'Elixir).

mkdir config && touch config/config.exs

Dans le fichier créer il faut ajouter les lignes suivantes en remplaçant le token par celui fourni par Botfather.

# config/config.exs

use Mix.Config

config :tesla, adapter: Tesla.Adapter.Hackney

# Remplacez ce token avec celui fourni par Botfather
config :ex_gram,
  token: "1287038449:AAHvSU4cGipZUH6i2Pbbhiz9akkE7_wP4YQ",
  adapter: ExGram.Adapter.Tesla,
  json_engine: Jason

Enfin on configure le module application.

# lib/macron_bot/application.ex

defmodule MacronBot.Application do
  use Application

  def start(_type, _args) do
    # Importe le token depuis la config
    token = ExGram.Config.get(:ex_gram, :token)

    children = [
      ExGram,
      # Lance le module MacronBot
      {MacronBot, [method: :polling, token: token]}
    ]

    # Si l'application rencontre un problème elle sera redémarrée
    opts = [strategy: :one_for_one, name: MacronBot.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

L'initialisation peut sembler longue, mais par la suite, on modifie très peu ces fichiers.

Création du bot

Afin d'explorer les possibilités techniques qui sont à notre disposition notre bot aura les fonctionnalités suivantes :

  • Une commande slash /parle qui affichera une citation au hasard (une commande slash est un "message qui commence par le signe /").
  • Lorsqu'on lui pose une question le bot affichera des boutons pour permettre à l'utilisateur de répondre
# lib/macron_bot.ex

defmodule MacronBot do
  @bot :Macron_Bot

  # Si on raisonne en Orienté Objet, on peut voir
  # le use comme "un héritage".
  # Le name serait un paramètre que l'on passe
  # au constructeur parent.
  use ExGram.Bot,
    name: @bot

  # Requis si vous ajoutez votre bot à un groupe
  middleware(ExGram.Middleware.IgnoreUsername)

  # Comme nous utilisons le module ExGram.Bot,
  # il est nécessaire d'implémenter une fonction
  # nommée handle pour que le bot puisse fonctionner.
  #
  # Le premier paramètre est un tuple dont les valeurs
  # changent suivant les cas.
  #
  # Le second paramètre est le context,
  # il contient des informations sur le message reçu
  def handle(_, context) do

    # La fonction answer nous permet d'envoyer un message
    # Telgram en tant que bot
    answer(context, "J'appelle à la responsabilité")
  end
end

Désormais on peut lancer le bot avec la commande.

iex -S mix

Le bot répondera "J'appelle à la responsabilité" à chacun de nos messages.

bot-hello

Les commandes slash

La commande slash parle affichera au hasard, des citations que j'ai piquée, sans vergogne, sur le site du Parisien.

slash-command

J'ai selectionné les phrases les plus croquignolesques mais bien sûr, adaptez selon vos envies.

# lib/macron_bot.ex

defmodule MacronBot do
  # Si l'on veut detecter une commande, le tuple
  # contiendra un atom :command à l'index 0.
  # Le nom de la commande à l'index 1.
  # Et le "message" à l'index 2,
  # c'est a dire le texte suivant la commande.
  def handle({:command, "parle", _msg}, context) do
    answers = [
      "Si j'étais chômeur, je n'attendrais pas tout de l'autre, j'essaierais de me battre d'abord.",
      "Je suis maoïste, [...] un bon programme c'est ce qui marche.",
      "Le libéralisme est une valeur de gauche.",
      "Il y a dans cette société une majorité de femmes. Il y en a qui sont, pour beaucoup, illettrées.",
      "Les Tontons Flingueurs, c'est un de mes films préférés. \"On n'est pas venus pour beurrer les sandwichs\" : ma réplique préférée.",
      "Make our planet great again !",
      "Vous n'allez pas me faire peur avec votre t-shirt, la meilleure façon de se payer un costard c'est de travailler.",
      "Je ne vais pas interdire Uber et les VTC, ce serait les renvoyer vendre de la drogue à Stains.",
      "Vu la situation économique, ne plus payer les heures supplémentaires c'est une nécessité.",
      "La tranche d'impôt de Hollande à 75 % ? C'est Cuba sans le soleil.",
      "Quand des pays ont encore sept à huit enfants par femmes, vous pouvez décider d'y dépenser des milliards d'euros, vous ne stabiliserez rien.",
      "Le kwassa-kwassa pêche peu. Il amène du Comorien.",
      "Lorsque la politique n'est plus une mission mais une profession, les politiciens deviennent plus égoïstes que les fonctionnaires.",
      "Ne laissez pas la critique de l'UE à ceux qui le détestent.",
      "L'audiovisuel public est la honte de la République.",
      "La politique sociale, regardez : on met un pognon de dingue dans des minimas sociaux, les gens sont quand même pauvres.",
      "Chaque pays a sa propre diplomatie. Faire partie de l'Europe ne signifie pas renoncer à son indépendance ou ne plus pouvoir prendre l'initiative.",
      "Je me bats sur le plan international pour qu'on arrive à faire baisser le prix du pétrole.",
      "Parce que c'est notre PROJEEEEET !!!"
    ]

    # On répond avec une phrase au hasard
    random_answer = answers |> Enum.random()

    answer(context, random_answer)
  end
end

Après avoir créé cette fonction, il faudra, au choix :

  • Couper le terminal et relancer iex -S mix
  • Taper recompile dans le terminal interactif précédemment ouvert

Question-réponse

Le mécanisme de question-réponse permet d'aborder plusieurs notions :

  • La détection de la question, détecter que la phrase se termine par un ?. On utilisera une REGEX.
  • Envoyer un message contenant des boutons
  • Détecter sur quel bouton l'utilisateur à cliqué

answer-question

Pour parvenir à ce comportement, notre code implémentera deux nouvelles méthode handle.

# lib/macron_bot.ex

defmodule MacronBot do
  # ...

  # On alias les models nécessaire pour construire
  # des messages contenant des boutons.
  alias Exgram.Model.{InlineKeyboardMarkup, InlineKeyboardButton}

  # Appelé lorsque l'on envoit un message textuel.
  def handle({:text, text, msg}, context) do

    # On teste si la phrase se termine
    # par un point d'interrogation.
    if String.match?(text, ~r/\?$/) do

      # On supprime le message que l'on vient d'envoyer.
      # Dans la pratique cela est optionnel,
      # mais on découvre une autre fonctionnalité 👍
      ExGram.delete_message(msg.chat.id, msg.message_id)

      # La fonction answer peut prendre un troisième paramètre
      # permettant d'enrichir le message. Des boutons ici.
      answer(context, text,
        reply_markup: %InlineKeyboardMarkup{
          inline_keyboard: [
            [
              %InlineKeyboardButton{
                text: "Oui",
                callback_data: "Oui"
              }
            ],
            [
              %InlineKeyboardButton{
                text: "Non",
                callback_data: "Non"
              }
            ]
          ]
        }
      )
    end
  end


  # Appelé lorsque l'on clique sur le bouton
  def handle({:callback_query, callback_query}, context) do
    # La valeur que l'on récupère via callback_query.data
    # est celle défini dans les callback_data ci-dessus
    answer(context, "#{callback_query.data}, mais en même temps")
  end
end

Bonus

Je vous donne le code pour créer une commande slash /covid. C'est cadeau, c'est pour moi. Emballé, c'est pesé !

# lib/macron_bot.ex

defmodule Macron do
  # ...

  def handle({:command, "covid", _msg}, context) do
    {:ok, covid_summary} = Tesla.get("https://api.covid19api.com/summary")

    json = covid_summary.body |> Jason.decode!()
    global = json |> Map.get("Global")

    france =
      json
      |> Map.get("Countries")
      |> Enum.find(fn country -> country["Slug"] == "france" end)

    covid_answer = ~s"""
    🌍
    Nouveaux cas : #{global["NewConfirmed"]}
    Nouvelles morts : #{global["NewDeaths"]}
    Cas totaux : #{global["TotalConfirmed"]}
    Morts totales : #{global["TotalDeaths"]}

    🇫🇷
    Nouveaux cas : #{france["NewConfirmed"]}
    Nouvelles morts : #{france["NewDeaths"]}
    Cas totaux : #{france["TotalConfirmed"]}
    Morts totales : #{france["TotalDeaths"]}
    """

    answer(context, covid_answer)
  end
end

Conclusion

A travers cet article j'espère avoir pu vous éclairer sur le fonctionnement de la librairie ex_gram. A titre personnel j'ai eu quelques difficultés a mettre en place certaines fonctionnalités, les réponses aux boutons par exemple. La documentation manque d'exemples à mon goût.

Néanmoins, la librairie reste bien conçue et une fois que l'on a compris la philosophie, on peut développer un bot très rapidement.

Je vous invite aussi à consulter la documentation Telegram afin de mieux comprendre d'ou viennent les structures utilisées.

Merci de m'avoir lu.

Liens

Oldest comments (3)

Collapse
 
meursmaximeblanc profile image
meursmaximeblanc

Très engagés vos exemples, Môsieur Smaug !

Collapse
 
jeansmaug profile image
jean-smaug

Je ne donne pas mon avis, je ne fais que relayer.

Mon dernier article sur le fonctionnement d'une monnaie comme l'euro, lui était clairement engagé !

Collapse
 
chloeveux profile image
chloeveux

C'est un exemple simple mais fonctionnel. Des idées sur le développement futur? Existe-t-il un hôte gratuit pour les projets d'élixir? Merci.