Un bot pour Mastodon écrit en Micropython

Il existe déjà sur Mastodon, le réseau social libre et décentralisé dont j’ai déjà parlé à plusieurs reprises, toute une série de bots plus ou moins intéressants. Certains donnent l’évolution du nombre d’utilisateurs et d’instances sur le réseau, d’autres tootent automatiquement un résumé d’articles de blogs quand ils sont publiés, ou publient régulièrement des photos coquines. Bref, il y en a pour tous les goûts.

Moi, j’ai décidé d’écrire un bot… qui ne sert à rien! Non, l’intérêt de mon bot ne se situe pas dans sa fonctionnalité, mais plutôt dans sa conception : il va s’exécuter sur un processeur embarqué.

Dans le but de rendre le travail simple et rapide, j’ai choisis de le développer en Micropython. Et pour l’exécuter, mon choix s’est porté sur la carte Lopy de Pycom, basée sur un processeur ESP32, avec WiFi intégré.

Mon MicroMastoBot (oui, c’est le nom que je lui ai donné :D) va donc effectuer les tâches suivantes:

  • Se connecter à internet via mon point d’accès WiFi
  • Effectuer un première requête de debug, afin de voir si la connection à l’instance Mastodon fonctionne
  • Écouter le flux de toots jusqu’à en détecter un où il [le bot] est mentionné
  • Envoyer un toot à l’émetteur du toot précédent
  • Se remettre à l’écoute des toots

Concrètement, pour effectuer toutes ces tâches, nous allons avoir besoin de :

  • L’API de Mastodon. Il est relativement simple, et est basée sur des requêtes HTTP POST et GET.
  • Le module ujson, afin d’encoder et décoder du JSON.
  • Le module urequest (légèrement modifié) afin d’envoyer des requêtes HTTP et d’en recevoir les réponses

Envoyer une requête HTTP GET

Une première requête HTTP GET que nous pouvons faire à l’instance Mastodon est de lui demander quelques informations (description, nom, version,…).

La requête est donc :

GET /api/v1/instance

Cela donne en Micropython:

response = urequest.get("https://mastodon.codingfield.com/api/v1/instance", headers={'Authorization': 'Bearer 1111111122222222333333334444444455555555666666667777777788888888'}    )
if response is not None:
    print(response.text)
else:
    print("No response")

Je reviendrai sur le header ‘Bearer’ plus tard dans l’article.

L’instance va répondre quelque chose comme:

{"uri":"mastodon.codingfield.com","title":"Mastodon::codingfield","description":"Welcome on Mastodon::Codingfield.\nThis
 instance is hosted by <a href=\"https://mastodon.codingfield.com/@JF\">@JF</a>, the owner of <a href=\"https://codingfi
eld.com\">Codingfield.com</a>.\nAnyone is welcomed on this instance, as long as they stay polite, friendly and respectfu
l.\n\n<hr />\nBienvenue sur Mastodon:Codingfield.\nCette instance est hébergée par <a href=\"https://mastodon.codingfiel
d.com/@JF\">@JF</a>, l'auteur du blog <a href=\"https://codingfield.com\">Codingfield.com</a>.\nTout le monde est le bie
nvenu sur cette instance, à condition de rester polis, amical et respectueux.","email":"jf@codingfield.com","version":"1
.3.3"}

Envoyer une requête HTTP POST

Une requête HTTP POST est nécessaire pour pouvoir publier un toot sur le réseau.

Voici la requête à émettre:

POST /api/v1/statuses

Ce qui donne en Micropython:

response = urequest.post('https://mastodon.codingfield.com/api/v1/statuses?status=Hello+@'+tootUser+',+this+toot+was+sent+by+a+Lopy+board+based+on+ESP32+and+running+Micropython&visibility=direct', headers={'Authorization': 'Bearer 1111111122222222333333334444444455555555666666667777777788888888'})
if response is not None:
    print(response.text)
else:
    print("No response")

Je reviendrai sur le header ‘Bearer’ plus tard dans l’article.

Écouter le flux de messages de l’instance

Cela semble à première vue très simple : il suffit d’attendre un message du serveur, de le traiter, et d’éventuellement envoyer un toot en réponse.

La requête à émettre pour demander au serveur de recevoir le flux de messages est :

GET /api/v1/streaming/user

Lorsqu’il reçoit cette requête, le serveur ne va pas répondre et fermer la connexion directement comme il le fait pour la plupart des autres requêtes. Au contraire, il va maintenir la connexion ouverte, et va simplement utiliser cette connexion pour envoyer les messages quand ils sont publiés. On est donc dans un modèle de communication ‘publish-subscribe’.

Cependant, maintenir une connexion ouverte longtemps sans y faire transiter de données n’est pas idéal. En effet, quand aucune donnée n’est envoyée par le serveur, le client ne peut pas savoir si le serveur est toujours présent ou non. Et vice-versa pour le serveur, qui ne peut pas savoir si son client est toujours effectivement connecté ou non.

Du coup, l’instance Mastodon va envoyer régulièrement des messages de heartbeat, c’est à dire des messages ‘vides’ qui permettent de maintenir la connexion ouverte, et de détecter la détection inopinée du client ou du serveur.

Le problème est que le module urequest que j’ai utilisé pour implémenter la communication HTTP ne semble pas supporter ce mode de fonctionnement. En effet, urequest ferme automatiquement la connexion dès que la réponse à la requête a été lue. C’est là qu’intervient la modification que j’ai effectuée sur le module. J’y ai ajouté un mode ‘stream’ .

Dans ce mode, une méthode de callback est appelée quand une réponse est reçue sur la connexion. Une fois l’exécution de ce callback terminée, le module se remet à l’écoute sur la connexion pour recevoir le message suivant.

Dans mon cas, c’est dans la méthode de callback que le message est traité afin de voir si il faut envoyer un toot en réponse.

Voici donc ce que donne l’envoie de cette requête HTTP en Micropython:

def onData(r):
    ret = True

    strInput = r.text()
  
    # Messages beginning with # should not be processed
    if strInput[0] == ':':
        return ret

    print("Message received : ")
    print(str(r.text()))
    return ret

r = urequest.get("https://mastodon.codingfield.com/api/v1/streaming/user", headers={'Authorization': 'Bearer 1111111122222222333333334444444455555555666666667777777788888888'}, stream=onData    )

Remarquez donc le paramètre ‘stream’, qui spécifie à la methode get() quel fonction appeler quand elle reçoit une réponse.

Ce callback reçoit lui en paramètre la réponse reçue afin de pouvoir la traiter et… retourne un booléen. Pourquoi? Pour fermer la connexion quand cela est nécessaire !

Sans cela, la méthode get() resterait à l’écoute de réponses à l’infini… sans nous donner l’opportunité de pouvoir faire autre chose, comme envoyer un toot! Donc, dans son implémentation, le bot retourne False quand il reçoit un toot où il est mentionné. Cela ferme la connexion ‘stream‘, et rends la main à la boucle principale du programme afin d’envoyer la requête pour envoyer le toot de réponse.

Authentification, revenons sur le Bearer

Ah, ce cher Bearer! A quoi sert-il? Il permet au serveur d’authentifier l’émetteur des requêtes, afin de s’assurer qu’il a bien les permissions nécessaires pour écouter les toots, en envoyer,… Cela fait visiblement partie du processus OAuth, que je ne connais pas du tout!.

Voici comment obtenir ce Bearer (par facilité, je l’ai fais depuis une machine Linux classique, en ligne de commande):

curl -X POST -d "client_name=Lopy1&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read write" https://mastodon.codingfield.com/api/v1/apps

--> {{"id":19,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffhhhhhhhh","client_secret":"iiiiiiiijjjjjjjjkkkkkkkkllllllllmmmmmmmmmnnnnnnnnoooooooopppppppp"}


curl -X POST -d "client_id=aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffhhhhhhhh&client_secret=iiiiiiiijjjjjjjjkkkkkkkkllllllllmmmmmmmmmnnnnnnnnoooooooopppppppp&grant_type=password&username=xxxxxxxx@mastodon.codingfield.com&password=********&scope=read+write" https://mastodon.codingfield.com/oauth/token

-->{"access_token":"1111111122222222333333334444444455555555666666667777777788888888","token_type":"bearer","scope":"read write","created_at":1495128278}

La réponse « access_token » contient le ‘Bearer’ qu’il faut spécifier dans les headers HTTP de toutes les requêtes envoyées au serveur.

Le code

J’ai publié le code source du bot sur mon compte github.

Laisser un commentaire

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