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

SMSとJavaでサンタを追跡する

最終更新日 May 10, 2021

所要時間:1 分

2004年12月から グーグルは毎年、クリスマスをテーマにしたサイトを提供し、クリスマス・イブの間、ユーザーがサンタを追跡できるようにしている。さらに NORADは1955年からサンタを追跡している。公式APIは存在しないが 非公式APIがあり、それを使ってサンタの居場所を追跡することができる。

クリスマスシーズンを祝して、私は Spring Bootアプリケーションを作りたいと思いました。このJavaアプリケーションの完全なコードは GitHub.

まず、アプリケーションがどのように機能するのかをより高いレベルで説明したい。それから、より困難な問題やそれを回避する方法について説明する。

実物を見る

Nexmo番号でSMSを受信すると、登録したWebhookにペイロードが送信されます。送信者の電話番号やメッセージの内容などの情報は、メッセージにどのように応答するかを決定するために使用されます。

実際に使ってみるとこんな感じだ:

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

最初のメッセージ

最初にメッセージを受け取ったとき、ユーザーには常にサンタの現在地が表示される。また、距離情報を得るために郵便番号を入力するかどうかも尋ねられる。

ロケーション検索

位置情報の検索は簡単な作業ではありません。郵便番号は世界中で統一されているわけではないので、ユーザがどの国から連絡してきたかを知ることは、検索を絞り込むのに役立つ。

ユーザーが郵便番号情報を提供することを選択した場合、私は次のものを使用します。 ネクスモ・ナンバー・インサイトを使って、ユーザーがメッセージを送っている国を調べます。その後、郵便番号の入力を求められます。ここから GeoNamesというサービスを使って、その郵便番号の緯度と経度を調べます。

この情報は電話番号とともにデータベースに保存される。緯度と経度は、サンタと彼らの距離を計算するのに使われる:

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

よりテクニカルに

表面的には、それほど複雑なアプリケーションには見えない。しかし、開発過程でぶつかった課題がかなりある。コードの技術的な側面をいくつか紹介したい。

着信メッセージのルーティング

私のNexmo番号は、Webhook URLにリクエストを送信するように設定されている。 POSTリクエストをウェブフックURLに送信するように設定されています。私は IncomingMessageController受信メッセージを処理するように設定されています:

@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);
}

この findOrCreatePhoneメソッドは、新しい 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
}

このエンティティには、電話番号、Nexmo番号、国コード、そしてユーザーが現在いるステージが含まれる。この Stage列挙については後のセクションで説明します。

この findHandlerメソッドは、適切な KeywordHandlerを検索してメッセージを処理します:

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

各ハンドラーは特定のキーワードの処理を担当する。アプリケーションが知っているキーワードは以下の通りである:

  • ヘルプこれは、ユーザーを支援するための文脈情報を提供する。

  • キャンセル現在の一連の質問をキャンセルします。

  • REMOVEこれはデータベースからユーザーを削除します。

  • はいの場合、ユーザーはその質問に対して肯定的な回答をします。

  • いいえで、ユーザーはその質問に対して否定的な回答をする。

  • WHEREで、サンタの現在地を応答する。

  • ロケーションこれは、ユーザーが自分の位置を更新できるようにするものです。

それぞれ KeywordHandlerはSpring Managed Beanとして登録される。例えば HelpKeywordHandlerは例えば次のようになる:

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

すべての KeywordHandler実装はすべて keywordHandlerマップに注入される。 IncomingMessageController:

private final Map<String, KeywordHandler> keywordHandlers;

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

マップに注入するとき、Springは camelCaseクラス名をキー、インスタンス化されたクラスを値とします。例えば HelpKeywordHandlerhelpKeywordHandler:

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

適切な KeywordHandler戦略パターン.有効な KeywordHandlerが見つからなければ DefaultKeywordHandlerが使われる。

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";
}

このようにメッセージをルーティングすることで、新しいキーワードを追加する必要がある場合に柔軟に対応できる。

さまざまなステージへの対応

各ハンドラーは、そのキーワードで始まるメッセージを担当する。しかし YesKeywordHandlerユーザがどの質問に答えているのかを知ることができるのだろうか?そこで Stageクラスの中の Phoneクラスのenumの出番です。

エンティティは Phoneエンティティは以下の中間段階に存在することができる:

  • ステージなしnull)は、作成されたばかりで、まだ質問されていないユーザーのためのものです。

  • INITIAL最初のメッセージにサンタの居場所と郵便番号を記入するかどうかのプロンプトが表示され、返信されたユーザー向け。

  • COUNTRY_PROMPT国番号が正しいかどうかを尋ねられたユーザーのために。

  • POSTAL_PROMPT郵便番号の入力を求められたユーザー用。

質問が終わると、次のような最終段階に入る:

  • REGISTERED郵便番号を入力したユーザーには、より詳細な情報が表示されます。

  • GUEST郵便番号情報の提供を希望しないユーザーには、サンタの現在地のみが表示され、距離は計算されません。

このように YesKeywordHandlerはステージを使用する:

@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);
  }
}

ステージで回答しているユーザーは INITIALステージで答えているユーザーは、"郵便番号情報を提供しますか?"という質問に答えているに違いありません。

で答えているユーザーは COUNTRY_PROMPTという質問に答える必要があります。YES、2文字の国番号、または気が変わった場合はCANCELと答えてください。"

国番号の取得

郵便番号をより正確に調べるには、ユーザーのメッセージの発信国を知ることが役立ちます。そこで PhoneLocationLookupServiceを作成し、Nexmo Basic Number Insightを使って電話番号の国情報を調べました:

@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;
    }
  }
}

このアプリケーションは、旅行中に別の国に設定したい場合に備えて、ユーザーに国の確認を求める。

郵便番号の検索

郵便番号の緯度経度情報を調べるのは簡単な作業ではないことがわかった。私がGeoNamesを選んだのは、サービスが無料だからである。

郵便番号情報を検索するために PostCodeLookupServiceを作成した。この getLocationメソッドはまず、データベース内の郵便番号についてすでに知っているかどうかを調べます。これは、サード・パーティ・サービスへの呼び出し回数を制限するのに役立つので、良いプラクティスです。

既存の Locationエンティティが存在しない場合は、サードパーティのサービスが呼び出され、新しいエンティティが永続化されます。郵便番号に一致する場所が見つからなかった場合は、空の Optionalを返し、アプリケーションはユーザーに再試行するように指示します:

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));
}

距離の計算

ユーザーの緯度と経度がわかれば、サンタの現在地を調べ、数式を使ってユーザーがサンタからどのくらい離れているかを判断することができる。これは 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;
}

結論

今回は、SMSでサンタの位置情報を取得するアプリケーションを作成する方法について説明しました。キーワードに基づいたメッセージのルーティング、ユーザーがどの質問に答えているかを判断するための複数のステージの使用、電話番号の国コードを取得するためのNexmo Number Insightの使用について説明しました。

より詳細な情報については、以下のコードを参照されたい。 GitHub.この READMEファイルには、アプリケーションを立ち上げて実行するために必要なすべての情報が含まれています。

ぜひ Googleのサンタトラッカーまたは NORADサンタトラッカーをぜひチェックしてください。

シェア:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Steve Crowヴォネージの卒業生

スティーブは自称数学者で、悪口の王様。グレイハウンド、曲がりくねったパズル、ヨーロッパのボードゲームをこよなく愛する。 非数学系の人には数学を、非Java系の人にはJavaの話をしていないときは、コーヒーを飲みながらコードをハックしている。