Compartir:
Steve se autoproclama matemático y rey de la sátira. También le gustan los galgos, los rompecabezas enrevesados y los juegos de mesa europeos. Cuando no está hablando de matemáticas con gente que no es matemática o de Java con gente que no es de Java, se le puede encontrar tomando café y hackeando código.
Rastrear a Papá Noel con SMS y Java
Desde diciembre de 2004, Google ofrece cada año un sitio con temática navideña que permite a los usuarios seguir a Papá Noel durante la Nochebuena. Además, NORAD lleva rastreando a Papá Noel desde 1955. Aunque no existe una API oficial, hay una API no oficial que se puede utilizar para rastrear el paradero de Papá Noel.
Con motivo de las fiestas navideñas, quería crear una aplicación de Spring Boot que pudiera utilizarse para obtener información actualizada sobre la ubicación de Papá Noel a través de SMS. El código completo de esta aplicación Java se puede encontrar en GitHub.
En primer lugar, quiero explicar cómo funciona la aplicación a un nivel superior. A continuación, nos adentraremos en algunos de los problemas más complicados y en cómo resolverlos.
Véalo en acción
Cuando se recibe un SMS en mi número Nexmo, se envía una carga útil a mi webhook registrado. Información como el número de teléfono del remitente y el contenido del mensaje se utilizan para determinar cómo responder al mensaje.
Así se ve en acción:
The santa tracker in action.
El mensaje inicial
La primera vez que se recibe un mensaje, siempre se muestra al usuario la ubicación actual de Papá Noel. También se le pregunta si desea indicar su código postal para obtener información sobre la distancia.
Buscar ubicación
La búsqueda de la ubicación no es una tarea trivial. Los códigos postales no son uniformes en todo el mundo, y ayuda saber desde qué país contacta el usuario para acotar nuestra búsqueda.
Si el usuario opta por facilitar información sobre el código postal, utilizo Nexmo Number Insight para buscar el país desde el que envían los mensajes. A continuación, se les pide que faciliten su código postal. A partir de ahí, utilizo un servicio llamado GeoNames para buscar la latitud y longitud de ese código postal.
Esta información se guarda en la base de datos junto con su número de teléfono. La latitud y la longitud se utilizan para calcular a qué distancia se encuentra Santa Claus:
A response with Santa's location
Más técnica
A primera vista, la aplicación no parece tan complicada. Sin embargo, durante el proceso de desarrollo me encontré con algunos problemas. Me gustaría destacar algunos de los aspectos más técnicos del código.
Enrutamiento de mensajes entrantes
Mi número Nexmo está configurado para enviar POST solicitudes a una URL de webhook. Tengo una IncomingMessageController configuración para manejar los mensajes entrantes:
@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);
}El método findOrCreatePhone se utiliza para crear una nueva Phone o para buscar una entidad existente. Este es el aspecto de la entidad teléfono:
@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
}La entidad contiene el número de teléfono, el número Nexmo, un código de país y la etapa actual en la que se encuentra el usuario. Hablaré del Stage enum en una sección posterior.
El método findHandler se utiliza para buscar un método KeywordHandler para gestionar el mensaje:
public interface KeywordHandler {
void handle(Phone phone, String text);
}Cada manejador es responsable de manejar una palabra clave específica. Las palabras clave que conoce la aplicación son:
AYUDA que proporciona información contextual para ayudar al usuario.
CANCELAR que cancela la serie actual de preguntas.
ELIMINAR que elimina el usuario de la base de datos.
SÍ en la que el usuario responde afirmativamente a la pregunta.
NO en la que el usuario responde negativamente a la pregunta.
DONDE que responde con la ubicación actual de Papá Noel.
UBICACIÓN que permite al usuario actualizar su ubicación.
Cada KeywordHandler se registra como un Spring Managed Bean. El HelpKeywordHandlerpor ejemplo, tiene este aspecto:
@Component
public class HelpKeywordHandler implements KeywordHandler {
@Override
public void handle(Phone phone, String text) {
// Logic here
}
}Todas las KeywordHandler implementaciones se inyectan en un keywordHandler en el mapa IncomingMessageController:
private final Map<String, KeywordHandler> keywordHandlers;
@Autowired
public IncomingMessageController(Map<String, KeywordHandler> keywordHandlers) {
this.keywordHandlers = keywordHandlers;
}Al inyectar en un mapa, Spring utilizará camelCase nombres de clase como clave y la clase instanciada como valor. Por ejemplo, la clase HelpKeywordHandler se almacena con la clave helpKeywordHandler:
HelpKeywordHandler handler = keywordHandlers.get("helpKeywordHandler");El KeywordHandler se elige utilizando una variante del Patrón de estrategia. Si no se encuentra KeywordHandler válido, se utiliza DefaultKeywordHandler para responder.
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";
}Enrutar los mensajes de esta manera permite flexibilidad cuando hay que añadir nuevas palabras clave.
Gestión de las distintas etapas
Cada manejador es responsable de los mensajes que comienzan con su palabra clave. Sin embargo, ¿cómo sabe el YesKeywordHandler sabe a qué pregunta está respondiendo el usuario? Aquí es donde el Stage dentro de la clase Phone de la clase.
La Phone entidad puede existir en las siguientes etapas intermedias:
Sin etapa (
null) para usuarios que acaban de ser creados y a los que aún no se les ha formulado ninguna pregunta.INITIALpara los usuarios a los que se ha respondido con el primer mensaje que contiene la ubicación de Papá Noel y una pregunta sobre si desean o no proporcionar un código postal.COUNTRY_PROMPTpara usuarios a los que se ha preguntado si su código de país es correcto.POSTAL_PROMPTpara los usuarios a los que se ha pedido un código postal.
Una vez formuladas las preguntas, se pasan a las siguientes fases finales:
REGISTEREDpara los usuarios que hayan facilitado un código postal y recibirán información más detallada.GUESTpara los usuarios que no deseen proporcionar información sobre el código postal y sólo recibirán la ubicación actual de Papá Noel sin calcular la distancia.
Así es como el YesKeywordHandler utiliza las etapas:
@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 usuario que está respondiendo en la INITIAL etapa debe estar respondiendo a la pregunta "¿Desea proporcionar información sobre el código postal?".
Un usuario que responda en la COUNTRY_PROMPT debe estar respondiendo a la pregunta "Veo que estás enviando un mensaje desde EE.UU.. Responde SÍ, un código de país de 2 caracteres, o CANCELAR si has cambiado de opinión".
Obtener el código de país
Para buscar el código postal con mayor precisión, es útil conocer el país desde el que envía el mensaje el usuario. He creado un PhoneLocationLookupService que utiliza Nexmo Basic Number Insight para buscar la información del país del número de teléfono:
@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;
}
}
}La aplicación pedirá al usuario que confirme su país por si se va de viaje y quiere establecerse en otro distinto.
Búsqueda de códigos postales
Resulta que buscar información sobre latitud y longitud de los códigos postales no es una tarea trivial. Decidí trabajar con GeoNames porque el servicio es gratuito.
He creado un PostCodeLookupService para buscar la información del código postal. El método getLocation busca primero si ya conocemos el código postal en la base de datos. Esta es una buena práctica, ya que ayuda a limitar el número de llamadas al servicio de terceros.
Si no existe una entidad Location se llama al servicio de terceros y se crea una nueva entidad. Si no podemos encontrar una ubicación que coincida con ese código postal, devolvemos un campo vacío Optional para que la aplicación sepa que debe pedir al usuario que vuelva a intentarlo:
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));
} Cálculo de la distancia
Una vez que tenemos la latitud y longitud del usuario, podemos buscar la ubicación actual de Papá Noel y utilizar una fórmula para determinar a qué distancia se encuentra el usuario de Papá Noel. Esto se hace en el método 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;
} Conclusión
Este fue un vistazo a cómo se podría crear una aplicación que le permite obtener actualizaciones de la ubicación de Santa Claus a través de SMS. Cubrimos el enrutamiento de mensajes basados en palabras clave, el uso de múltiples etapas para determinar a qué preguntas responde un usuario y el uso de Nexmo Number Insight para obtener el código de país de un número de teléfono.
Para obtener información más detallada, recomiendo consultar el código en GitHub. El archivo README contiene toda la información necesaria para poner en marcha la aplicación.
Echa un vistazo a Rastreador de Papá Noel de Google o Rastreador de Papá Noel de NORAD en Nochebuena para estar al tanto de la llegada de Papá Noel.
Compartir:
Steve se autoproclama matemático y rey de la sátira. También le gustan los galgos, los rompecabezas enrevesados y los juegos de mesa europeos. Cuando no está hablando de matemáticas con gente que no es matemática o de Java con gente que no es de Java, se le puede encontrar tomando café y hackeando código.