LINE BOTをJava Servletでお手軽に作る その２：画像メッセージやテンプレートを試してみた

LINE BOTをJavaのServletを利用してお手軽に作る方法を紹介します
紹介したソースコード一式は https://github.com/riversun/line-bot-servlet-examples.git にあります

前回投稿「[LINE BOTをJava Servletでお手軽に作る](http://qiita.com/riversun/items/d5c418c66ac51a8e26cf)」の続編です。

# LINE BOTとのやりとりのバリエーションを増やす
今回は、[前回](http://qiita.com/riversun/items/d5c418c66ac51a8e26cf)紹介したテキストメッセージ以外にも画像やダイアログなど４つ試してみました。

ひきつづきLINEの[Messaging API](https://devdocs.line.me/ja/#messaging-api)を活用して、LINE BOTを**スッピンのJava Servlet**として実装していきます

# 【その１】BOTからのお返事として画像を返す

以下のように、BOTからのお返事として画像を返す例を紹介します

**実行例**
![example02.png](https://qiita-image-store.s3.amazonaws.com/0/170905/63b70db7-ebdd-d7b6-363f-a23433bf3d2c.png)


**ソースコード**

```java:LineBotExample02Servlet.java
@SuppressWarnings("serial")
public class LineBotExample02Servlet extends LineBotServlet {

	private static final String CHANNEL_SECRET ="取得したものをいれる";
	private static final String CHANNEL_ACCESS_TOKEN ="取得したものをいれる";

	@Override
	protected ReplyMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException {

		// ユーザーがBOTに送信したメッセージ
		TextMessageContent userMessage = event.getMessage();

		// ユーザーのProfileを取得する
		UserProfileResponse userProfile = getUserProfile(event.getSource().getUserId());

		// BOTからの返信メッセージ
		String botResponseText = userProfile.getDisplayName() + "さん、"
				+ "「" + userMessage.getText() + "」って言いましたね";

		// 返信用テキストメッセージ
		TextMessage textMessage = new TextMessage(botResponseText);

		// 返信用画像(image)のURL
		final String originalContentUrl = "https://riversun.github.io/img/riversun_256.png";
		final String previewImageUrl = "https://riversun.github.io/img/riversun_144.png";

		// 返信用画像(image)メッセージ
		ImageMessage imageMessage = new ImageMessage(originalContentUrl, previewImageUrl);

		// テキストメッセージと画像を同時に返す
		return new ReplyMessage(event.getReplyToken(), Arrays.asList(imageMessage, textMessage));
	}

	@Override
	protected ReplyMessage handleDefaultMessageEvent(Event event) {
		// overrideしていないメッセージを受信した場合は何もしない(nullを返す)
		return null;
	}

	@Override
	public String getChannelSecret() {
		return CHANNEL_SECRET;
	}

	@Override
	public String getChannelAccessToken() {
		return CHANNEL_ACCESS_TOKEN;
	}

}

```

### 解説

画像を返すのは非常にシンプルで、
以下のように画像のURL(original,previewの2種類。両方同じものを指定してもよい)を指定してImageMessageオブジェクトをつくります。

```java

　 // 返信用画像(image)のURL
        final String originalContentUrl = "https://riversun.github.io/img/riversun_256.png";
        final String previewImageUrl = "https://riversun.github.io/img/riversun_144.png";

        // 返信用画像(image)メッセージ
        ImageMessage imageMessage = new ImageMessage(originalContentUrl, previewImageUrl);
```

そして、以下のように、リプライするだけです。

```java

        // テキストメッセージと画像を同時に返す
        return new ReplyMessage(event.getReplyToken(), Arrays.asList(imageMessage, textMessage));
```


``Arrays.asList(imageMessage, textMessage)``のようにメッセージをList<>で指定すると、その指定順でBOTから複数のメッセージが返信されます。

# 【その２】：ユーザーがBOTに対して画像を送信する
次はユーザーから送信された画像をBOT側が受信して処理する例です。

**実行例**

このように、ユーザーが送信した画像をBOT側でファイルに保存します。
![example03.png](https://qiita-image-store.s3.amazonaws.com/0/170905/30b84d72-f079-4a3d-c0ac-fdb713ab447a.png)


**ソースコード**

```java:LineBotExample03Servlet.java

public class LineBotExample03Servlet extends LineBotServlet {

	private static final String CHANNEL_SECRET ="取得したものをいれる";
	private static final String CHANNEL_ACCESS_TOKEN ="取得したものをいれる";

	@Override
	protected ReplyMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException {
		return new ReplyMessage(event.getReplyToken(), Arrays.asList(new TextMessage("画像を送ってね")));
	}

	@Override
	protected ReplyMessage handleImageMessageEvent(MessageEvent<ImageMessageContent> event) throws IOException {

		// Contentについてはこちら https://devdocs.line.me/ja/#content
		InputStream is = getContentStream(event.getMessage());

		// 画像ファイルの保存先パス
		String tempImageFilePath = System.getProperty("user.dir") + "/" + "line_img_" + System.currentTimeMillis() + ".jpg";

		// ファイルを保存
		Files.copy(is, Paths.get(tempImageFilePath));

		return new ReplyMessage(event.getReplyToken(), Arrays.asList(new TextMessage("画像をありがとう！")));

	}

	@Override
	protected ReplyMessage handleDefaultMessageEvent(Event event) {
		// overrideしていないメッセージを受信した場合は何もしない(nullを返す)
		return null;
	}

	@Override
	public String getChannelSecret() {
		return CHANNEL_SECRET;
	}

	@Override
	public String getChannelAccessToken() {
		return CHANNEL_ACCESS_TOKEN;
	}
```
### 解説
この例では、ユーザー画像![chara2.png](https://qiita-image-store.s3.amazonaws.com/0/170905/09008ebd-d21b-08f7-382d-72ce16fe4e81.png)をおくり、BOTがそれを受信して反応を返します。

ソースコードでは、ユーザーから画像を受け取るために、**LineBotServlet#handleImageMessageEvent**をオーバーライドしています。

```java

@Override
	protected ReplyMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException
```

この例のキモはここで、

```java
InputStream is = getContentStream(event.getMessage());
```
送られた来た画像のストリームを取得します。

内部の処理に少しふれておくと、LINEのAPIとしては、ユーザーから画像が送られてくる時は[Webhook event](https://devdocs.line.me/ja/#webhook-event-object)としてImageMessageがWebhook URLにPOSTされる、という動作になりますが、Imagemessage自体は**画像が送られてくるよ**ということだけが通知され、実際の画像は送信されてこない仕組みです。

そのため、ImageMessageをうけとったら、そこに記載されているIDをつかって、別途画像をダウンロードしにいく必要があり、それが[Content](https://devdocs.line.me/ja/#content)というAPIです

上の`getContentStream(event.getMessage());`では[Content](https://devdocs.line.me/ja/#content)APIにアクセスして画像をダウンロード（ダウンロードするためのストリームをひっぱる）をしています。

[API](https://devdocs.line.me/ja/#content)の説明をよむと
> コンテンツはメッセージが送信されてからある期間経過した時点で自動的に削除されます。保存される期間についての保証はありません。

とあるので、はやめにダウンロードしておく必要があります。

なおこの仕組みは画像だけでなく、動画や音声といったサイズの大きなコンテンツを取り扱う際に毎度つかわれます。

この例では、ダウンロードした画像ファイルは、**カレントディレクトリ**に保存します。

```java
String tempImageFilePath = System.getProperty("user.dir") + "/" + "line_img_" + System.currentTimeMillis() + ".jpg";

Files.copy(is, Paths.get(tempImageFilePath));
```

<hr>

# 【その３】テンプレートメッセージをつかって複数の選択肢の中から選ぶ例
次はテンプレート・メッセージを使ってみます。

テンプレートメッセージは https://devdocs.line.me/ja/#template-message に詳しいですが
>Template messageは、あらかじめ定義されたレイアウトのテンプレートにカスタムデータを挿入することによって構築するメッセージ

です。iOS版およびAndroid版のLINE 6.7.0以降で対応しており、今は複数の選択肢の中から１つ選ぶようなダイアログ・ウィジェットのような見た目をつくるテンプレートが利用可能です。

商品選択やホテル予約など、ある決められたフローに従って処理をすすめていくようなシーンで活躍しそうです。

**実行例**

今回は、好きな料理を１つ選択する以下のようなインタフェースをつくってみました。

![example04.png](https://qiita-image-store.s3.amazonaws.com/0/170905/db6cf27f-d9d4-af08-999d-c39fe4fbe86b.png)

**ソースコード**

```java:LineBotExample04Servlet.java

public class LineBotExample04Servlet extends LineBotServlet {

	private static final String CHANNEL_SECRET ="取得したものをいれる";
	private static final String CHANNEL_ACCESS_TOKEN ="取得したものをいれる";

	@Override
	protected ReplyMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException {

		UserProfileResponse userProfile = getUserProfile(event.getSource().getUserId());

		String thumbnailImageUrl = "https://riversun.github.io/img/riversun_256.png";

		String title = "料理のジャンル選択";
		String text = userProfile.getDisplayName() + "さんは、どんな料理が好きですか？";

		Action japaneseCuisine = new PostbackAction("和食", "japanese");
		Action italianCuisine = new PostbackAction("イタリアン", "italian");
		Action frenchCuisine = new PostbackAction("フレンチ", "french");

		List<Action> actions = Arrays.asList(japaneseCuisine, italianCuisine, frenchCuisine);

		ButtonsTemplate buttonsTemplate = new ButtonsTemplate(thumbnailImageUrl, title, text, actions);

		String altText = title;

		return new ReplyMessage(event.getReplyToken(), new TemplateMessage(altText, buttonsTemplate));
	}

	@Override
	protected ReplyMessage handlePostbackEvent(PostbackEvent event) {
		// ButtonsTemplateでユーザーが選択した結果が、このPostBackEventとして返ってくる

		PostbackContent postbackContent = event.getPostbackContent();

		// PostbackActionで設定したdataを取得する
		String data = postbackContent.getData();

		final String replyText;

		if ("japanese".equals(data)) {
			replyText = "和食がお好きなんですね。";
		} else if ("italian".equals(data)) {
			replyText = "イタリアン、良いですよね。";
		} else {
			replyText = "フレンチ、私も食べたいです。";
		}

		return new ReplyMessage(event.getReplyToken(), Arrays.asList(new TextMessage(replyText)));
	}

	@Override
	protected ReplyMessage handleDefaultMessageEvent(Event event) {
		// overrideしていないメッセージを受信した場合は何もしない(nullを返す)
		return null;
	}

	@Override
	public String getChannelSecret() {
		return CHANNEL_SECRET;
	}

	@Override
	public String getChannelAccessToken() {
		return CHANNEL_ACCESS_TOKEN;
	}

}
```
### 解説
テンプレートは、今のところ3つ用意されています。
- [Buttons](https://devdocs.line.me/ja/#buttons) 複数のボタン(4つまで)と画像をレイアウトしたもの
- [Confirm](https://devdocs.line.me/ja/#confirm) YES,NOのような2択を選択させるもの
- [Carousel](https://devdocs.line.me/ja/#carousel) 選択肢のセットを並べて提示するもの

テンプレートには選択肢を設置できますが、それに相当するのが[アクション](https://devdocs.line.me/ja/#template-action)です。

[アクション](https://devdocs.line.me/ja/#template-action)は今のところ3つ用意されています
- Postback action タップするとpostback eventが発生する
- Message action タップするとユーザーの発言として送信される
- URI action タップするとブラウザで指定したURIが開く

この例では、テンプレートとして、[Buttons Template](https://devdocs.line.me/ja/#buttons)を使用しました。

アクションはPostbackActionを指定しました。

```java
 Action japaneseCuisine = new PostbackAction("和食", "japanese");
        Action italianCuisine = new PostbackAction("イタリアン", "italian");
        Action frenchCuisine = new PostbackAction("フレンチ", "french");

        List<Action> actions = Arrays.asList(japaneseCuisine, italianCuisine, frenchCuisine);
```

PostbackActionがタップされると、内部的には[Postback event](https://devdocs.line.me/ja/#postback-event)が発生しますので、
これを受け取るために``handlePostbackEvent``をオーバーライドしました。

```java
 protected ReplyMessage handlePostbackEvent(PostbackEvent event) 
```

何がタップされたかは、`` new PostbackAction("イタリアン", "italian")``の第２引数として指定したdataを以下のように取得して判定します。

```java
String data = postbackContent.getData();
```

これで固定メッセージの選択インタフェースができました。

<hr>



# 【その４】テンプレートメッセージの選択肢をリンクにする例
前の例では、選択肢がすべてPostbackActionでしたが、ここではタップするとWebサイトにジャンプするURIActionをつかっていきます。

**実行例**
![example05.png](https://qiita-image-store.s3.amazonaws.com/0/170905/afe924db-f355-7895-6c41-f3018ca92466.png)

選択肢(ここでは"Google"か"Bing")をタップすると、
↓のように指定したWebサイト（インライン）にジャンプします。

![example05_b.png](https://qiita-image-store.s3.amazonaws.com/0/170905/59bc0f61-b933-0eac-6b9b-e595da420282.png)


**ソースコード**

```java:LineBotExample05Servlet.java
public class LineBotExample05Servlet extends LineBotServlet {

	private static final String CHANNEL_SECRET ="取得したものをいれる";
	private static final String CHANNEL_ACCESS_TOKEN = "取得したものをいれる";

	@Override
	protected ReplyMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException {

		String thumbnailImageUrl = "https://riversun.github.io/img/riversun_256.png";

		String title = "好きな検索エンジン";
		String text = "好きな検索エンジンを選んでください";

		Action google = new URIAction("Google", "https://www.google.co.jp");
		Action bing = new URIAction("Bing", "https://www.bing.com");
		Action other = new PostbackAction("その他", "other");

		List<Action> actions = Arrays.asList(google, bing, other);

		ButtonsTemplate buttonsTemplate = new ButtonsTemplate(thumbnailImageUrl, title, text, actions);

		String altText = title;

		return new ReplyMessage(event.getReplyToken(), new TemplateMessage(altText, buttonsTemplate));
	}

	@Override
	protected ReplyMessage handlePostbackEvent(PostbackEvent event) {

		PostbackContent postbackContent = event.getPostbackContent();

		String data = postbackContent.getData();

		if ("other".equals(data)) {
			return new ReplyMessage(event.getReplyToken(), new TextMessage("他の検索エンジンが好きなんですね。"));
		} else {
			return null;
		}

	}

	@Override
	protected ReplyMessage handleDefaultMessageEvent(Event event) {
		// overrideしていないメッセージを受信した場合は何もしない(nullを返す)
		return null;
	}

	@Override
	public String getChannelSecret() {
		return CHANNEL_SECRET;
	}

	@Override
	public String getChannelAccessToken() {
		return CHANNEL_ACCESS_TOKEN;
	}

}
```

### 解説

URIActionはタップすると指定したURIでブラウザが開きます。
アクションは複数の種類を混ぜることも可能なので、以下ではURIActionとPostbackActionを混在させてみました。

```java
Action google = new URIAction("Google", "https://www.google.co.jp");
        Action bing = new URIAction("Bing", "https://www.bing.com");
        Action other = new PostbackAction("その他", "other");

        List<Action> actions = Arrays.asList(google, bing, other);
```

<hr>


# ローカルPCでお手軽に試す
このサンプルもサーバーやクラウドにアップロードせずに、Jettyとngrokを活用してローカルでサクっと試せます。

Gradle/Mavenに以下を追加します。

### Gradle
**compile 'org.eclipse.jetty:jetty-server:9.4.0.v20161208'**
**compile 'org.eclipse.jetty:jetty-webapp:9.4.0.v20161208'**

### Maven
```xml

<dependency>
	<groupId>org.eclipse.jetty</groupId>
	<artifactId>jetty-server</artifactId>
	<version>9.4.0.v20161208</version>
</dependency>
<dependency>
	<groupId>org.eclipse.jetty</groupId>
	<artifactId>jetty-webapp</artifactId>
	<version>9.4.0.v20161208</version>
</dependency>
```

### Jettyを起動する
Gradle/Mavenに記述したら、以下のようにコードを書いて実行しましょう。
コメントアウトしているところは適宜コメント解除すると上で紹介したサンプルが動作します。

```java:AppMain.java
public class AppMain {

	public static void main(String[] args) throws Exception {

		ServletHandler handler = new ServletHandler();

		handler.addServletWithMapping(LineBotExample01Servlet.class, "/callback");
		//handler.addServletWithMapping(LineBotExample02Servlet.class, "/callback");
		//handler.addServletWithMapping(LineBotExample03Servlet.class, "/callback");
		//handler.addServletWithMapping(LineBotExample04Servlet.class, "/callback");
		//handler.addServletWithMapping(LineBotExample05Servlet.class, "/callback");

		// loclahost:3000でJettyを起動
		Server jetty = new Server(3000);

		jetty.setHandler(handler);
		jetty.start();
		jetty.join();

	}

}
```

### 外部からアクセスできるようにする
せっかくローカルで立ち上げても、LINE BOTとして振舞うには、サーバーにDeployして外部に公開する必要があります。
テスト用途でお手軽にためすため、さきほどJettyをつかってつくったサーバーを外部からアクセスできるようにしましょう。

ここでは、こうした用途にぴったりな ngrok というサービスを使います。

以下から ngrok をダウンロードしておきます
https://ngrok.com/download

コマンドラインで
``ngrok http -region=ap 127.0.0.1:3000``

とすると、ngrokが開始して以下のような画面が表示されます
![ngrok.png](https://qiita-image-store.s3.amazonaws.com/0/170905/261552c2-314e-85da-2a84-0cabd9751c0d.png)

たったこれだけで、
https://xxxxx.ap.ngrok.io というurlが https://127.0.0.1:3000 にマップされました！

_（xxxxxの部分はngrokを起動するたびにランダムに変わります。sign upすれば固定することも可能です)_

LINE BOTのweb hook urlにはhttpsが求められますのでこれは便利ですね。

ここまでくれば、あとは、以下のurlをWeb Hook URLに登録するだけです。
``https://xxxxx.ap.ngrok.io/callback``


# 【まとめ】
- [前回](http://qiita.com/riversun/items/d5c418c66ac51a8e26cf)につづき、JavaのスッピンのHttpServletをつかって、LINE BOT APIを試してみました。
- テキストメッセージ以外のメッセージリプライも、わりとお手軽につくることができました。
- 紹介したソースコード一式は https://github.com/riversun/line-bot-servlet-examples.git にあります