Principe

L’envoi de notifications peut être perçu comme un envoi unidirectionnel d’information d’une source vers une destination. Pratique pour alerter d’une situation ou prévenir de l’état d’un système. L’envoi d’image en plus de texte est souvent le bienvenu ; l’information peut alors prendre des formats différents.

Logiciels

L’objectif est d’intégrer un système de notifications à un smartphone, pourvu d’un échange sécurisé et dont le contenu n’est pas stocké sur un serveur tiers. Que de contraintes à première vue, dans la mesure où l’on doit passer par le principe clients-serveur dont le serveur a le rôle de relai. Mais en dressant un rapide état des lieux, on note l’existence de différents logiciels prévus pour cela, tels que ntfy [0], gotify [1], et d’autres avec lesquels on peut construire un système de notification, tels que Telegram [2], Signal [3],… Qui peut le plus peut le moins : si deux interlocuteurs peuvent communiquer entre eux, il est alors possible d’en définir un comme la source de l’information et le second comme la destination.

Ce qui compte ici sont bien les fonctionnalités et caractéristiques du serveur ; et si le protocole d’échange est standard on pourra alors utiliser n’importe quel logiciel client standard qui parle ce protocole – typiquement curl qui dialogue en HTTP.

Voici un bref comparatif des différentes propositions. Il n’est pas exhaustif, et ne prend pas en compte toutes les solutions de messagerie instantanée dont le détail est proposé par Mark Williams sur ce site web [4].

LogicielÉchange sécuriséEnvoi d’image attachéeStockage du contenuDisponibilitéOpen sourceActivité
NtfyOuiOuiLocalAndroid, iOSOuiActif
GotifyOuiNon, image distanteDistantAndroid, FdroidOuiActif
TelegramOuiOuiDistantAndroid, iOSApps oui, Serveur nonPas de bilan d’activité du serveur
SignalOuiOuiLocalAndroid, iOSOuiActif

Choix d’une solution

Pour plusieurs raisons, je me tourne vers l’utilisation de Signal pour envoyer des notifications :

  • Open source,
  • Échange sécurisé de bout-en-bout,
  • Pas de serveur à héberger moi-même, le rendre accessible et tout ce que cela implique,
  • Facilité d’intégration : beaucoup de gens autour de moi utilisent Signal, cela peut être intéressant d’intégrer un système de notification dans un de mes groupes.

Mise en place

La mise en place de l’écosystème Signal nécessite :

  • La création d’un compte Signal, uniquement par numéro de téléphone,
  • L’enregistrement de ce numéro,
  • La persistence de l’environnement Signal ainsi obtenu.

On peut se tourner vers le package Debian [5] embarquant l’utilitaire signal-cli [6]. Une image Docker est disponible, proposant un environnement avec ce package. On pourra de ce fait travailler de manière isolée dans un conteneur tout le long de cette mise en place. Commençons par récupérer l’image.

 $ docker pull registry.gitlab.com/packaging/signal-cli/signal-cli-jre

Enregistrement du numéro

La difficulté réside surtout dans l’appropriation d’un nouveau numéro de téléphone… Que ce soit via une carte prépayée ou via des offres en ligne de fournisseur d’accès internet français, on en revient au même constat : il faut sortir le portefeuille.

Dès qu’un numéro – désigné NUMBER pour la suite — peut être enregistré, on peut lancer la commande register de signal-cli, via un conteneur Docker. On monte un volume pour faire persister la configuration Signal et les détails d’enregistrement.

 $ docker run -v ${PWD}/data:/root/.local/share/signal-cli:rw -it registry.gitlab.com/packaging/signal-cli/signal-cli-jre:latest -a  "$NUMBER" register

À l’issue de cette étape, un lien vers la résolution d’un captcha est affiché sur stdout. On doit résoudre le captcha et récupérer le token via les developer tools du navigateur, comme le précise la documentation de signal-cli [7].

To get the token, go to https://signalcaptchas.org/registration/generate.html For the staging environment, use: https://signalcaptchas.org/staging/registration/generate.html Check the developer tools for a redirect starting with signalcaptcha:// Everything after signalcaptcha:// is the captcha token.

Lorsque le captcha est copié dans le presse-papier, on peut le coller dans la ligne de commande suivante.

 $ docker run -v ${PWD}/data:/root/.local/share/signal-cli:rw -it registry.gitlab.com/packaging/signal-cli/signal-cli-jre:latest -a  "$NUMBER" register --captcha "$CAPTCHA"

On terminera la procédure d’enregistrement avec la vérification par le code SMS automatiquement reçu sur le numéro NUMBER.

 $ docker run -v ${PWD}/data:/root/.local/share/signal-cli:rw -it registry.gitlab.com/packaging/signal-cli/signal-cli-jre:latest -a  "$NUMBER" verify "$CODE"

Persistence des données

Les différents appels Docker montent toujours le même volume ${PWD}/data:/root/.local/share/signal-cli:rw . C’est ce répertoire que l’on devra monter dans les prochains conteneurs pour envoyer des messages.

L’envoi de messages

On peut d’ores et déjà envoyer des messages à un second numéro TARGET préalablement enregistré sur Signal :

$ docker run -v ${PWD}/data:/root/.local/share/signal-cli:rw -it registry.gitlab.com/packaging/signal-cli/signal-cli-jre:latest -a "$NUMBER" send -m "Hello, World !" "$TARGET"
WARN  ManagerImpl - No profile name set. When sending a message it's recommended to set a profile name with the updateProfile command. This may become mandatory in the future.
1234567890

Et voilà, en se contentant de bash, l’envoi de notifications sécurisé de bout-en-bout vers un utilisateur ou un groupe Signal est possible.

Automatisation et APIs

Mais se contenter de bash n’est parfois pas une simple tâche. Si l’on veut automatiser l’envoi de certains contenus avec des règles un peu mieux conçues, on va vite se retrouver perdu. C’est à ce moment que je me tourne vers le langage Python et cherche des bibliothèques existantes, proposées par la communautée. En prenant partie de la bibliothèque signalbot [8], je vais pouvoir intégrer l’envoi de messages à un programme Python. Le prérequis principal est d’avoir accès à une API de type JSON-RPC qui wrap signal-cli.

Qui parle de serveur, parle d’accès et d’ouverture de port… Cependant il n’y a pas de soucis ici : le serveur peut être mis en place localement sans accès extérieur. On va pouvoir lancer un conteneur Docker avec les détails d’enregistrement, en mode JSON-RPC, avec le docker-compose.yml suivant. Attention, on change d’image Docker [9], conformément à la documentation de la bibliothèque signalbot [10]. On la récupère dans un premier temps.

 $ docker pull bbernhard/signal-cli-rest-api:latest

Et on peut lancer la commande docker-compose up -d dans le même répertoire que le ficher suivant.

version: "3"
services:
  signal-cli:
    image: bbernhard/signal-cli-rest-api:latest
    volumes:
      - /home/antoine/signal_bot/data:/home/.local/share/signal-cli:rw # chemin absolu vers le dossier persistent d'enregistrement
    environment:
      - MODE=json-rpc # supported modes: json-rpc, native, normal

On peut créer un réseau Docker dédié pour maîtriser plus sereinement l’adressage du conteneur des APIs. Ce sera pratique lorsque l’on donnera cette adresse à notre programme Python qui s’y connectera.

Voici un exemple de classe Python pour se connecter aux APIs et de ce fait envoyer des messages.

#!/usr/bin/env python3

from signalbot import SignalBot
[...]

class SignalBotManager(Thread):
    def __init__(self):
        super().__init__()
        self._bot = None
        self._recipient = None
        self._config = None

    def set_bot_config(self, service:str, phone_number:str, recipient:str) -> None:
        self._config = {
            "signal_service": service,
            "phone_number": phone_number,
            "storage": None,
        }
        self._recipient = recipient

    def get_bot(self) -> Optional[SignalBot]:
        return self._bot

    def get_recipient(self) -> Optional[str]:
        return self._recipient

    def setup(self) -> bool:
        try:
            self._bot = SignalBot(self._config)
        except Exception as e:
            logging.warning(str(e))
            return False
        else:
            return True

    async def send_images(self, message:str, images:list) -> bool:
        try:
            await self._bot.start_typing(self._recipient)
            await self._bot.send(
                    self._recipient,
                    message,
                    base64_attachments=images,
                )
        except Exception as e:
            logging.warning(str(e))
            return False
        else:
            return True
        finally:
            await self._bot.stop_typing(self._recipient)

    def run(self) -> None:
        self._bot.listen(self._recipient)
        self._bot.start()

Le programme Python peut quant à lui instancier cette classe et ensuite envoyer une image :

# create the bot manager in the main thread
bm = SignalBotManager()
bm.set_bot_config(
	service="API_IP:PORT", # l'adresse IP du serveur JSON-RPC
	phone_number="+33123456789", # NUMBER
	recipient="+33987654321", # TARGET
	)
bm.setup()
bm.run()

[...]

# in another thread
loop = asyncio.new_event_loop()
loop.run_until_complete(bm.send_images(
        message=message,
        base64_attachments=[encoded_image.decode("utf-8")]
    )
)
loop.close()

Une attention particulière sera apportée au package Python asyncio qui est utilisé par la bibliothèque signalbot. On devra s’adapter à ce comportement.

Conclusion

Au terme de cette procédure, on dispose d’un moyen de notification basé sur des logiciels open source. Le contenu de l’information est maîtrisé, autant pour son transfert que pour son stockage.


Resources :

[0] https://ntfy.sh/

[1] https://gotify.net/

[2] https://telegram.org/

[3] https://www.signal.org/

[4] https://www.securemessagingapps.com/

[5] https://packaging.gitlab.io/signal-cli/

[6] https://github.com/AsamK/signal-cli/

[7] https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc#register

[8] https://pypi.org/project/signalbot/

[9] https://hub.docker.com/r/bbernhard/signal-cli-rest-api/

[10] https://github.com/filipre/signalbot-example