SMS groupés avec Twilio et Java

August 20, 2021
Rédigé par
Révisé par

SMS groupés avec Twilio et Java

La vie est suffisamment compliquée comme ça : pas besoin de rajouter le transfert intempestif des messages entre amis et membres de la famille pour s'assurer que tout le monde est informé de ce qui se passe. Dans ce post, je vais vous expliquer comment j'ai configuré un numéro Twilio que j'ai donné comme étant « mon » numéro de téléphone aux écoles de mes enfants afin que tous les messages reçus et envoyés depuis ce numéro soient automatiquement transférés à ma femme et moi, pour que l'un ou l'autre puisse répondre. Je suis sûr que vous pouvez imaginer des situations dans votre vie où cela serait pratique : livraison de colis, planification d'événements, rappels de rendez-vous, la liste est longue.

Dans ce post, je vais utiliser Java, mais la même approche fonctionne avec n'importe quel langage dans lequel vous pouvez construire une application Web. Si vous êtes à l'aise avec JavaScript, le transfert de SMS vers plusieurs numéros sur Code Exchange est un bon point de départ.

Que construisons-nous ?

Tout est basé sur un seul numéro de téléphone Twilio et un groupe, composé de vous et des membres de votre famille ou de vos amis, qui utilisent des téléphones mobiles. Vous pouvez donner votre numéro Twilio à d'autres personnes comme s'il s'agissait de votre propre numéro.

Nous allons configurer le numéro Twilio pour que n'importe quel message texte reçu par ce numéro soit transféré à tous les membres de votre groupe. Je vais utiliser deux membres de groupe dans cet exemple, mais le code est conçu pour que vous puissiez en utiliser autant que vous le souhaitez.

Schéma d'un téléphone envoyant un SMS vers un numéro Twilio, transféré vers deux autres téléphones

Lorsque quelqu'un en dehors de votre groupe (le téléphone de gauche) envoie un SMS au numéro Twilio, le message est transféré à tous les membres du groupe (les téléphones de droite). Les messages s'afficheront comme s'ils avaient été envoyés à partir du numéro Twilio. Le numéro de téléphone de l'expéditeur réel sera donc ajouté au début du message.

Lorsqu'un membre de votre groupe répond au numéro Twilio, il doit ajouter le numéro du destinataire réel au début du message, qui sera supprimé avant le transfert du message. Tous les membres de votre groupe reçoivent également une copie du message :

Schéma de l'un des téléphones du schéma précédent répondant au SMS. Le message est envoyé à tous les participants.

Notez que si vous voulez envoyer un message d'un membre du groupe à tous les autres, vous pouvez le faire en ajoutant votre propre numéro au début du message.

Prérequis

Pour construire ce système, vous aurez besoin des éléments suivants :

Utilisation de Twilio Programmable Messaging

Pour indiquer à Twilio comment se comporter en réponse à des SMS entrants, nous utiliserons webhooks. Lorsqu'un message est reçu par notre numéro de téléphone, Twilio envoie une requête HTTP à une URL que nous fournissons. Nous construisons une application pour renvoyer des instructions dans la réponse HTTP indiquant à Twilio quelle est la marche à suivre.

Même schéma que le précédent avec l'ajout d'une requête/réponse HTTP de Twilio à « votre application »

Les instructions de la réponse HTTP sont écrites en TwiML, avec plusieurs balises message qui indiquent à Twilio d'envoyer de nouveaux messages texte. Le contenu et la destination de ces messages dépendent de la personne qui a envoyé le message entrant et de ce qu'elle a écrit : des attributs qui font partie de la requête HTTP. Lisez la suite pour découvrir comment construire cette application à l'aide de Java et de Spring Boot

Construire l'application

Spring Boot est le framework le plus populaire pour construire des applications Web en Java. Généralement, je commence de nouveaux projets avec Spring Initializr. Si vous souhaitez suivre le processus de codage, utilisez ce lien qui définit les mêmes options que celles que j'ai utilisées. Sinon, vous trouverez le projet terminé sur GitHub.

Téléchargez, décompressez et ouvrez le projet généré dans votre IDE. Il y aura une seule classe dans src/main/java, dans le package com.example.smsgroupbroadcast, appelée SmsGroupBroadcastApplication. Vous n'aurez pas besoin de modifier cette classe, mais elle contient une méthode main que vous pouvez utiliser pour exécuter l'application.

Dans le même package, créez une nouvelle classe appelée SmsHandler, dans un nouveau fichier appelé SmsHandler.java. Pour éviter que les choses ne deviennent trop compliquées, nous allons mettre tout notre code dans cette classe. Une fois terminé, il contiendra environ 100 lignes.

Commencez par le code qui doit être exécuté au démarrage :


@RestController
public class SmsHandler {

   private final Set<String> groupPhoneNumbers;

   public SmsHandler() {
       groupPhoneNumbers = Set.of(System.getenv("GROUP_PHONE_NUMBERS").split(","));
   }

// more code will go in here

}

[ce code avec les importations sur GitHub]

L'annotation @RestController indique à Spring que cette classe doit être analysée pour rechercher des méthodes capables de traiter les requêtes HTTP. Nous allons bientôt en rédiger une.

La zone Set<String> groupPhoneNumbers à la ligne 4 est initialisée dans le constructeur en lisant une variable d'environnement dont la valeur est une chaîne de numéros de téléphone séparés par des virgules au format E.164. Ces numéros doivent inclure votre téléphone mobile et les téléphones de tous les autres membres de votre groupe (du côté droit dans les schémas ci-dessus). Vous pouvez définir des variables d'environnement directement dans votre IDE :

Capture d&#x27;écran montrant comment définir des variables d&#x27;environnement dans IntelliJ IDEA

Configuration de variable d'environnement IntelliJ IDEA

Méthode de traitement des requêtes HTTP

Pour faciliter l'écriture correcte en TwiML, nous utiliserons la librairie de Twilio pour Java. Ajoutez l'extrait de code suivant dans la section <dependencies> de pom.xml, le fichier de configuration Maven qui se trouve tout en haut de votre projet :

<dependency>
  <groupId>com.twilio.sdk</groupId>
  <artifactId>twilio</artifactId>
  <version>8.18.0</version>
</dependency>

Nous vous recommandons toujours d'utiliser la dernière version des librairies Twilio. Au moment de la rédaction, la dernière version est 8.18.0 et vous pouvez vérifier si des versions plus récentes existent sur mvnrepository.com.

Vous devrez peut-être indiquer à votre IDE d'actualiser les modifications de Maven à ce stade. Ajoutez ensuite ce code à votre classe SmsHandler :


@RequestMapping(
    value = "/sms",
    method = {RequestMethod.GET, RequestMethod.POST},
    produces = "application/xml")
@ResponseBody
public String handleSmsWebhook(
        @RequestParam("From") String fromNumber,
        @RequestParam("To")   String twilioNumber,
        @RequestParam("Body") String messageBody) {

    List<Message> outgoingMessages;

    if (groupPhoneNumbers.contains(fromNumber)) {
            outgoingMessages = messagesSentFromGroup(fromNumber, twilioNumber, messageBody);

    } else {
            outgoingMessages = messagesSentToGroup(fromNumber, twilioNumber, messageBody);
    }

    MessagingResponse.Builder responseBuilder = new MessagingResponse.Builder();
    outgoingMessages.forEach(responseBuilder::message);
    return responseBuilder.build().toXml();
}

[ce code avec les importations sur GitHub]

Cette méthode commence par de nombreuses annotations reconnues par Spring :

  • @RequestMapping indique à Spring que cette méthode doit être utilisée pour les requêtes GET et POST à /sms et que le Content-type de la réponse est application/xml.
  • @ResponseBody indique à Spring que la valeur de retour de cette méthode doit être utilisée comme le corps de la réponse HTTP.
  • Les annotations @RequestParam demandent à Spring d'extraire les paramètres nommés de la requête HTTP et de les transmettre comme arguments à la méthode. Cela fonctionne à la fois pour GET et POST même si les paramètres se trouvent dans différentes parties de la requête HTTP.

Le corps de la méthode crée une liste d'objets Message qui est remplie différemment selon que le SMS entrant qui a déclenché ce webhook provient ou non d'un membre du groupe (ligne 12). Nous allons définir les méthodes messagesSentFromGroup et messagesSentToGroup dans un instant, mais notez tout d'abord la façon dont la liste des Messages est ajoutée à une MessagingResponse en utilisant forEach sur les lignes 19 à 21.

Traitement des messages provenant de personnes en dehors du groupe

Si la vérification groupPhoneNumbers.contains(fromNumber) dans la méthode ci-dessus renvoie false, alors nous savons que le message vient de quelqu'un en dehors de notre groupe. Dans ce cas, la méthode messagesSentToGroup est appelée pour obtenir une liste d'objets Message représentant une copie du message entrant à transférer à chaque membre du groupe :

private List<Message> messagesSentToGroup(String fromNumber, String twilioNumber, String messageBody) {

    List<Message> messages = new ArrayList<>();

    String finalMessage = "From " + fromNumber + " " + messageBody;
    groupPhoneNumbers.forEach(groupMemberNumber ->
                messages.add(createMessageTwiml(groupMemberNumber, twilioNumber, finalMessage))
    );

    return messages;
}

[ce code avec les importations sur GitHub]

Nous construisons le finalMessage et ajoutons un Message à la liste pour chaque membre du groupe. J'ai créé une petite méthode d'aide appelée createMessageTwiml pour transformer le code du modèle de construction de la librairie Twilio en une seule ligne. J'ai pensé que cela en valait la peine car nous allons construire des objets Message plusieurs fois dans cette classe. Cette méthode se présente comme suit :

private Message createMessageTwiml(String to, String from, String body) {
    return new Message.Builder()
        .to(to)
        .from(from)
        .body(new Body.Builder(body).build())
        .build();
}

[ce code avec les importations sur GitHub]

Messages des membres du groupe

Lorsqu'un membre du groupe envoie un message au numéro Twilio, il doit le faire précéder du numéro de destination réelle :

Un SMS disant « +336xxxxx Merci ! »

La partie « Merci ! » du message sera envoyée depuis le numéro Twilio vers le numéro au début du message. Tout le monde dans le groupe en aura aussi une copie. Pour cela, la méthode messagesSentFromGroup doit diviser le corps du message, vérifier s'il commence par un numéro de téléphone (si ce n'est pas le cas, envoyer un rappel) et créer une liste de messages sortants, comme ceci :


private List<Message> messagesSentFromGroup(String fromNumber, String twilioNumber, String messageBody) {
    List<Message> messages = new ArrayList<>();

    String[] messageParts = messageBody.split("\\s+", 2);

    String e164Regex = "\\+[0-9]+";
    if (messageParts.length != 2 || !messageParts[0].matches(e164Regex)) {
        return List.of(createHowToMessage(fromNumber, twilioNumber));
    }

    String realToNumber = messageParts[0];
    String realMessageBody = messageParts[1];

    // add the message to the non-group recipient
    messages.add(
        createMessageTwiml(realToNumber, twilioNumber, realMessageBody)
    );

    // send a copy of the message to everyone in the group except the sender
    String groupCopyMessage = "To " + realToNumber + " " + realMessageBody;
    groupPhoneNumbers.forEach(groupMemberNumber -> {
        if (!groupMemberNumber.equals(fromNumber)) {
            messages.add(
                createMessageTwiml(groupMemberNumber, twilioNumber, groupCopyMessage));
        }
    });

    return messages;
}

private Message createHowToMessage(String fromNumber, String twilioNumber){
    return createMessageTwiml(fromNumber, twilioNumber,
        "To send a message to someone outside your group, " +
        "don't forget to include the destination phone number at the start, " +
        "eg '+44xxxx Ahoy!'");
}

[ce code avec les importations sur GitHub]

La méthode messagesSentFromGroup peut sembler très fournie en code, mais elle se divise à peu près en deux parties. Les lignes 4 à 9 traitent de la séparation de l'entrée et de la vérification pour voir si le message commence par un numéro de téléphone. La e164Regex teste un + suivi de chiffres, ce qui correspond au formatage E.164 pour les numéros de téléphone.

Le reste de messagesSentFromGroup dresse une liste de tous les messages que nous devons envoyer et la renvoie.

Enfin, il existe une méthode distincte pour createHowToMessage qu'il valait, à mon avis, la peine de développer pour rendre plus lisible la méthode longue.

Code terminé

La classe SmsHandler est complète, le code est donc terminé. Pour référence, la classe complète se trouve sur GitHub.

Exécution locale de votre code

Le moyen le plus simple de démarrer l'application est d'utiliser votre IDE pour exécuter la méthode main dans la classe SmsGroupBroadcasterApplication que nous avons vue précédemment. Si vous préférez utiliser un terminal de ligne de commande, exécutez ./mvnw spring-boot:run tout en haut du projet. Dans les deux cas, n'oubliez pas de définir la variable d'environnement GROUP_PHONE_NUMBERS.

Une fois l'application démarrée, vous pouvez accéder à http://localhost:8080/sms?From=__from__&To=__to__&Body=__body__ et vous devriez voir une réponse du type :

<Response>
  <Message from="__to__" to="GROUP_MEMBER_1">
    <Body>From __from__ __body__</Body>
  </Message>
  <Message from="__to__" to="GROUP_MEMBER_2">
    <Body>From __from__ __body__</Body>
  </Message>
</Response>

GROUP_MEMBER_1 et GROUP_MEMBER_2 seront les numéros de votre variable d'environnement GROUP_PHONE_NUMBERS.

Utilisation de votre code avec Twilio

Pour que Twilio puisse utiliser votre application pour ses webhooks, il lui faudra une URL publique. Il existe de nombreuses façons de déployer du code Java en ligne, mais pour plus de simplicité, je vous recommande d'utiliser ngrok.

Après avoir installé ngrok, vous pouvez exécuter ngrok http 8080 et vous verrez une URL de transfert https que vous devrez définir pour le moment où « un message arrive » sur la page de configuration de votre numéro de téléphone. N'oubliez pas d'ajouter le chemin de /sms à l'URL :

Capture d&#x27;écran de la configuration du webhook « quand un message arrive » dans la console Twilio.

Vous pouvez également définir l'URL du webhook pour un numéro de téléphone à l'aide de CLI Twilio :

twilio phone-numbers:update <PHONE_NUMBER> --sms-url=<URL>

Si l'URL est une adresse localhost, l'interface de ligne de commande (CLI) Twilio créera un tunnel ngrok pour vous.

Si vous voulez tester le système avant de donner le vrai numéro, recrutez quelques amis avec des téléphones ou utilisez d'autres numéros Twilio pour essayer. Une fois satisfait, déplacez l'application vers un cloud public toujours actif pour ne pas avoir à maintenir en permanence votre machine de développement en fonctionnement. Je m'éloigne du sujet de ce post, mais la documentation de Spring sur les packages et le déploiement présente de nombreuses options. Vous n'avez besoin que d'une seule requête HTTP pour chaque SMS envoyé à votre numéro Twilio, les exigences sont donc très restreintes.

Pour aller plus loin

Vous pouvez apporter de nombreuses améliorations à cette application, par exemple :

  • Pour les grands groupes, il peut s'avérer utile d'inclure le numéro de l'expéditeur dans les messages en copie.
  • Ajoutez un « carnet d'adresses » dans votre application pour pouvoir ajouter le nom plutôt que le numéro lors de l'envoi à des destinataires en dehors du groupe.
  • Il n'existe aucune vérification pour s'assurer que les requêtes HTTP proviennent effectivement de Twilio. Il ne s'agit que d'un problème mineur pour cette application car si quelqu'un d'autre fait appel à votre application, il recevra du TwiML en retour, mais cela ne vous coûtera rien. Si vous voulez ajouter cette vérification, j'ai rédigé l'article Sécuriser vos webhooks Twilio en Java qui vous montrera comment faire.

N'hésitez pas à me faire part des projets que vous construisez avec Twilio ! Contactez-moi sur Twitter : @MaximumGilliard ou à l'adresse mgilliard@twilio.com. J'ai hâte de voir ce que vous allez construire !