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 : demo-telegram-bot-elixir.
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.
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épondra “J’appelle à la responsabilité” à chacun de nos messages.
Les commandes slash
La commande slash parle affichera au hasard, des citations que j’ai piqué, sans vergogne, sur le site du Parisien.
J’ai sélectionné 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 a cliqué
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’où viennent les structures utilisées.
Merci de m’avoir lu.