https://d226lax1qjow5r.cloudfront.net/blog/blogposts/track-santa-sms-java-dr/Tracking-Santa-with-SMS.png

Suivre le Père Noël avec SMS et Java

Publié le May 10, 2021

Temps de lecture : 6 minutes

Depuis décembre 2004, Google propose chaque année un site sur le thème de Noël qui permet aux utilisateurs de suivre le Père Noël pendant la nuit de Noël. En outre, NORAD suit le Père Noël depuis 1955. Bien qu'il n'existe pas d'API officielle, il existe une API non officielle non officielle qui peut être utilisée pour suivre les allées et venues du Père Noël.

Pour célébrer la période de Noël, j'ai voulu créer une application de Spring Boot qui pourrait être utilisée pour obtenir des mises à jour sur l'emplacement du Père Noël par SMS. Le code complet de cette application Java se trouve sur GitHub.

Tout d'abord, je souhaite expliquer le fonctionnement de l'application à un niveau plus élevé. Ensuite, nous pourrons nous pencher sur certains des problèmes les plus difficiles à résoudre et sur les moyens de les contourner.

Voyez-le en action

Lorsqu'un SMS est reçu sur mon numéro Numbers, une charge utile est envoyée à mon webhook enregistré. Des informations telles que le numéro de téléphone de l'expéditeur et le contenu du message sont alors utilisées pour déterminer comment répondre au message.

Voici ce que cela donne en pratique :

The santa tracker in action.The santa tracker in action.

Le message initial

Lors de la première réception d'un message, l'utilisateur reçoit toujours la position actuelle du Père Noël. Il lui est également demandé s'il souhaite indiquer son code postal pour obtenir des informations sur la distance.

Recherche de lieux

La recherche de lieux n'est pas une tâche triviale. Les codes postaux ne sont pas uniformes dans le monde entier et il est utile de savoir de quel pays l'utilisateur prend contact pour affiner notre recherche.

Si l'utilisateur choisit de fournir des informations sur le code postal, j'utilise Nexmo Number Insight pour rechercher le pays à partir duquel il envoie des messages. Il lui est alors demandé de fournir son code postal. À partir de là, j'utilise un service appelé GeoNames pour connaître la latitude et la longitude de ce code postal.

Ces informations sont enregistrées dans la base de données avec leur numéro de téléphone. La latitude et la longitude sont utilisées pour calculer la distance qui sépare le Père Noël de ces personnes :

A response with Santa's locationA response with Santa's location

Devenir plus technique

À première vue, l'application ne semble pas très compliquée. Cependant, j'ai rencontré un certain nombre de difficultés au cours du processus de développement. J'aimerais souligner certains des aspects les plus techniques du code.

Acheminement des messages entrants

Mon numéro Numbers est configuré pour envoyer des requêtes à une URL webhook. POST des requêtes à une URL webhook. J'ai une IncomingMessageController pour gérer les messages entrants :

@PostMapping
public void post(@RequestParam("msisdn") String from,
        @RequestParam("to") String nexmoNumber,
        @RequestParam("keyword") String keyword,
        @RequestParam("text") String text
) {
  Phone phone = findOrCreatePhone(from, nexmoNumber);
  findHandler(phone, keyword).handle(phone, text);
}

La méthode findOrCreatePhone est utilisée pour persister une nouvelle entité Phone ou pour consulter une entité existante. Voici à quoi ressemble l'entité téléphone :

@Entity
public class Phone {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String number;
  private String nexmoNumber;
  private String countryCode;

  @Enumerated(EnumType.STRING)
  private Stage stage;

  @OneToOne
  private Location location;

  // Getters and Setters
}

L'entité contient le numéro de téléphone, le numéro de Numbers, un code pays et l'étape actuelle dans laquelle se trouve l'utilisateur. Je parlerai de l'enum Stage dans une section ultérieure.

La méthode findHandler est utilisée pour rechercher un KeywordHandler approprié pour traiter le message :

public interface KeywordHandler {
  void handle(Phone phone, String text);
}

Chaque gestionnaire est chargé de traiter un mot-clé spécifique. Les mots-clés connus par l'application sont les suivants :

  • AIDE qui fournit des informations contextuelles pour aider l'utilisateur.

  • ANNULER qui annule la série de questions en cours.

  • REMOVE qui supprime l'utilisateur de la base de données.

  • OUI dans lequel l'utilisateur répond à la question par l'affirmative.

  • NON dans lequel l'utilisateur répond à la question par la négative.

  • qui répond en indiquant l'emplacement actuel du Père Noël.

  • LOCATION qui permet à l'utilisateur de mettre à jour sa position.

Chaque KeywordHandler est enregistré en tant que Spring Managed Bean. Le HelpKeywordHandlerpar exemple, ressemble à ceci :

@Component
public class HelpKeywordHandler implements KeywordHandler {
  @Override
  public void handle(Phone phone, String text) {
    // Logic here
  }
}

Toutes les KeywordHandler sont injectées dans une keywordHandler sur la carte IncomingMessageController:

private final Map<String, KeywordHandler> keywordHandlers;

@Autowired
public IncomingMessageController(Map<String, KeywordHandler> keywordHandlers) {
  this.keywordHandlers = keywordHandlers;
}

Lors de l'injection dans une carte, Spring utilise les noms de classe comme clé et la classe instanciée comme valeur. camelCase comme clé et la classe instanciée comme valeur. Par exemple, la classe HelpKeywordHandler est stockée avec la clé helpKeywordHandler:

HelpKeywordHandler handler = keywordHandlers.get("helpKeywordHandler");

Le choix de l'élément approprié est ensuite effectué à l'aide d'une variante de la méthode KeywordHandler est alors choisi en utilisant une variante du Modèle de stratégie. Si aucun KeywordHandler valide n'est trouvée, le DefaultKeywordHandler est utilisée pour répondre.

private KeywordHandler findHandler(Phone phone, String keyword) {
  // New users should always go to the default handler
  if (phone.getStage() == null) {
    return keywordHandlers.get("defaultKeywordHandler");
  }

  KeywordHandler handler = keywordHandlers.get(keywordToHandlerName(keyword));
  return (handler != null) ? handler : keywordHandlers.get("defaultKeywordHandler");
}

private String keywordToHandlerName(String keyword) {
  return keyword.toLowerCase() + "KeywordHandler";
}

L'acheminement des messages de cette manière permet une certaine flexibilité lorsque de nouveaux mots-clés doivent être ajoutés.

Gérer les différentes étapes

Chaque gestionnaire est responsable des messages qui commencent par son mot-clé. Cependant, comment le YesKeywordHandler sait-il à quelle question l'utilisateur répond ? C'est là qu'intervient l'élément Stage à l'intérieur de la classe Phone entre en jeu.

L'entité Phone L'entité peut exister aux stades intermédiaires suivants :

  • Aucune étape (null) pour les utilisateurs qui viennent d'être créés et à qui aucune question n'a encore été posée.

  • INITIAL pour les utilisateurs qui ont reçu une réponse avec le premier message contenant l'emplacement du Père Noël et un message leur demandant s'ils souhaitent ou non fournir un code postal.

  • COUNTRY_PROMPT pour les utilisateurs à qui l'on a demandé si leur code pays était correct.

  • POSTAL_PROMPT pour les utilisateurs à qui l'on a demandé de fournir un code postal.

Une fois les questions posées, elles sont soumises aux étapes finales suivantes :

  • REGISTERED pour les utilisateurs qui ont fourni un code postal et qui recevront des informations plus détaillées.

  • GUEST pour les utilisateurs qui n'ont pas souhaité fournir d'informations sur le code postal et qui recevront uniquement la position actuelle du Père Noël, sans calcul de la distance.

C'est ainsi que le YesKeywordHandler utilise les étapes :

@Override
public void handle(Phone phone, String text) {
  if (phone.getStage() == Phone.Stage.INITIAL) {
    handlePromptForCountryCode(phone);
  } else if (phone.getStage() == Phone.Stage.COUNTRY_PROMPT) {
    handlePromptForPostalCode(phone);
  } else {
    outgoingMessageService.sendUnknown(phone);
  }
}

Un utilisateur qui répond à l'étape INITIAL doit répondre à la question "Souhaitez-vous fournir des informations sur votre code postal ?"

Un utilisateur qui répond à l'étape COUNTRY_PROMPT doit répondre à la question "Je vois que vous envoyez un message depuis les États-Unis. Répondez OUI, un code de pays à 2 caractères, ou ANNULEZ si vous avez changé d'avis".

Obtenir le code pays

Afin de rechercher le code postal avec plus de précision, il est utile de connaître le pays d'où provient le message de l'utilisateur. J'ai créé un PhoneLocationLookupService qui utilise Nexmo Basic Number Insight pour rechercher les informations sur le pays du numéro de téléphone :

@Service
public class PhoneLocationLookupService {
  private final InsightClient insightClient;

  @Autowired
  public PhoneLocationLookupService(NexmoClient nexmoClient) {
    this.insightClient = nexmoClient.getInsightClient();
  }

  public String lookupCountryCode(String number) {
    try {
      return this.insightClient.getBasicNumberInsight(number).getCountryCode();
    } catch (IOException | NexmoClientException e) {
      return null;
    }
  }
}

L'application demandera à l'utilisateur de confirmer son pays au cas où il voyagerait et voudrait se placer dans un autre pays.

Recherche de codes postaux

Il s'avère que la recherche d'informations sur la latitude et la longitude des codes postaux n'est pas une tâche triviale. J'ai choisi de travailler avec GeoNames parce que le service est gratuit.

J'ai créé un PostCodeLookupService pour gérer la recherche des informations sur le code postal. La méthode getLocation cherche d'abord à savoir si nous connaissons déjà le code postal dans la base de données. C'est une bonne pratique car cela permet de limiter le nombre d'appels au service tiers.

S'il n'existe pas d'entité Location le service tiers est appelé et une nouvelle entité est maintenue. Si nous n'avons pas trouvé de lieu correspondant à ce code postal, nous renvoyons un champ vide Optional vide afin que l'application sache qu'il faut demander à l'utilisateur de réessayer :

private Optional<Location> getLocation(String country, String postalCode) {
  Optional<Location> locationOptional = locationRepository.findByPostalCode(postalCode);
  if (locationOptional.isPresent()) {
    return locationOptional;
  }

  LocationResponse response = getLocationResponse(country, postalCode);
  if (response.getPostalCodes().isEmpty()) {
    return Optional.empty();
  }

  Location newLocation = buildLocation(response, postalCode);
  return Optional.of(locationRepository.save(newLocation));
}

Calcul de la distance

Une fois que nous avons la latitude et la longitude de l'utilisateur, nous pouvons consulter la position actuelle du Père Noël et utiliser une formule pour déterminer la distance qui sépare l'utilisateur du Père Noël. Cette opération s'effectue dans la fonction DistanceCalculationService:

public double getDistanceInMiles(double lat1, double lng1, double lat2, double lng2) {
  double theta = lng1 - lng2;
  double dist = Math.sin(Math.toRadians(lat1))
          * Math.sin(Math.toRadians(lat2))
          + Math.cos(Math.toRadians(lat1))
          * Math.cos(Math.toRadians(lat2))
          * Math.cos(Math.toRadians(theta));

  dist = Math.acos(dist);
  dist = Math.toDegrees(dist);
  dist = dist * 60 * 1.1515;

  return dist;
}

Conclusion

Il s'agissait de voir comment on peut créer une application qui vous permet d'obtenir des mises à jour de l'emplacement du Père Noël par SMS. Nous avons abordé le routage des messages en fonction de mots clés, l'utilisation de plusieurs étapes pour déterminer à quelles questions un utilisateur répond, et l'utilisation de Nexmo Number Insight pour obtenir l'indicatif de pays d'un numéro de téléphone.

Pour des informations plus détaillées, je vous recommande de consulter le code sur GitHub. Le fichier README contient toutes les informations dont vous avez besoin pour démarrer avec l'application elle-même.

Ne manquez pas de jeter un coup d'œil sur La traque du Père Noël de Google ou NORAD Santa Tracker aux alentours de la veille de Noël pour suivre l'évolution du Père Noël.

Partager:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Steve CrowAnciens de Vonage

Steve est un mathématicien autoproclamé et le roi du sarcasme. Il aime aussi les lévriers, les puzzles tortueux et les jeux de société européens. Lorsqu'il ne parle pas de mathématiques à des personnes qui n'en font pas, ou de Java à des personnes qui n'en font pas, on peut le trouver en train de siroter un café et de bidouiller du code.