line-bot-sdk-java で chat bot


# 概要

「LINE Messaging API」が9月29日に公開されました。この新APIは以前の「LINE BOT API Trial Account」よりもかなりシンプルになっているのが特徴です。このAPIのリリースにあわせて、公式SDKが各言語で用意されています。今回はJava版を使って簡単なchat botを作ってみます。

## やりたいこと

テキストメッセージを受信すると、メッセージの種類に応じて声優の村川梨衣さんの画像をTwitterで検索して返信します。

GitHubは[こちら](https://github.com/rshindo/rietion-line-bot)

# 環境

* jdk1.8
* Spring Boot
* Heroku

# 下準備

LINE Messaging API の利用登録は[こちら](https://business.line.me/ja/)から行います。
アカウントを作成したら、Webhook送信が「利用する」になっていることを確認します。また、自動応答メッセージは「利用しない」にしておいたほうがいいかと思います。
![setup.PNG](https://qiita-image-store.s3.amazonaws.com/0/91613/5cf4abfd-506d-2903-9362-835a8b9d6c19.png)

Herokuアカウントの設定については本筋でないので省略します。。

# ソースコード

今回のchat botはSpring Bootで作ります。SDKにはSpring Boot用のプロジェクトがあり、それを利用すると非常に楽です。

## build.gradle 

```groovy:build.gradle
dependencies {
	compile('org.springframework.boot:spring-boot-starter-social-twitter')
	runtime('org.springframework.boot:spring-boot-devtools')
	compileOnly('org.projectlombok:lombok')
	testCompile('org.springframework.boot:spring-boot-starter-test')
	
	// LINE BOT SDK
	compile group: 'com.linecorp.bot', name: 'line-bot-spring-boot', version: '1.3.0'
}
```

[MVNRepository](https://mvnrepository.com/artifact/com.linecorp.bot/line-bot-spring-boot)にはversion 1.1.0までしか載っていないのですが、2016-11-16時点では1.3.0までありますので、そちらを使います。
Twitter Search APIも使うので、Spring Social Twitterも依存関係に入れています。

## エンドポイント

```java
import java.io.IOException;

import com.linecorp.bot.model.event.Event;
import com.linecorp.bot.model.event.MessageEvent;
import com.linecorp.bot.model.event.message.TextMessageContent;
import com.linecorp.bot.model.response.BotApiResponse;
import com.linecorp.bot.spring.boot.annotation.EventMapping;
import com.linecorp.bot.spring.boot.annotation.LineMessageHandler;

@LineMessageHandler
public class MessageHandler {

	private final ReplyMessageHandler replyMessageHandler;
	
	public MessageHandler(ReplyMessageHandler replyMessageHandler) {
		super();
		this.replyMessageHandler = replyMessageHandler;
	}

	@EventMapping
	public void handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws IOException {
		System.out.println("event: " + event);
		BotApiResponse response = replyMessageHandler.reply(event);
		System.out.println("Sent messages: " + response);
	}
	
	@EventMapping
	public void defaultMessageEvent(Event event) {
		System.out.println("event: " + event);
	}
	
}
```

APIのエンドポイントですが、Spring MVCの@Controller + @RequestMappingではなく、**@LineMessageHandler + @EventMapping**で定義します。
@LineMessageHandlerをクラスに、各メソッドに対して@EventMappingをつけます。このとき、送信されたメッセージのタイプに応じて、マッチする引数の型を持つメソッドをよしなに呼び出してくれます。
例えば上のコードの場合、テキストタイプのメッセージが送られたらhandleTextMessageEventが、それ以外のタイプであればdefaultMessageEventが呼び出されます。
なお、URLは/callbackとなります。

## リプライ送信

```java
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.constraints.NotNull;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.social.twitter.api.SearchParameters;
import org.springframework.social.twitter.api.SearchResults;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.stereotype.Service;

import com.linecorp.bot.client.LineMessagingService;
import com.linecorp.bot.model.ReplyMessage;
import com.linecorp.bot.model.event.MessageEvent;
import com.linecorp.bot.model.event.message.TextMessageContent;
import com.linecorp.bot.model.message.ImageMessage;
import com.linecorp.bot.model.message.Message;
import com.linecorp.bot.model.message.TextMessage;
import com.linecorp.bot.model.response.BotApiResponse;

import github.rshindo.rlb.util.TwitterApiUtils;

@Service
public class ReplyMessageHandler {
	
	private static final String RIETION = "りえしょん";
	private static final String MURAKAWA_RIE = "村川梨衣";
	private static final String ROBOT = "ロボ";
	
	@Value("${rietionbot.twitter.robot}")
	@NotNull
	private String robotAccountId; 
	
	@Value("${rietionbot.twitter.riemagic}")
	@NotNull
	private String riemagicAccountId;
	
	private final Twitter twitter;
	private final LineMessagingService lineMessagingService;
	
	public ReplyMessageHandler(Twitter twitter, LineMessagingService lineMessagingService) {
		super();
		this.twitter = twitter;
		this.lineMessagingService = lineMessagingService;
	}

	public BotApiResponse reply(MessageEvent<TextMessageContent> event) throws IOException {
		
		String receivedMessage = event.getMessage().getText();
		String replyToken = event.getReplyToken();
		List<Message> messages = null;
		switch (receivedMessage) {
			case RIETION:
				messages = searchRietionImages();
				break;
			case MURAKAWA_RIE:
				messages = searchRieMurakawaImages();
				break;
			case ROBOT:
				messages = searchRobotTexts();
				break;
			default:
				messages = searchRiemagicTexts();
				break;
		}
		return lineMessagingService
			.replyMessage(new ReplyMessage(replyToken, messages))
			.execute()
			.body();
		
	}
	
	protected List<Message> searchRietionImages() {
		return searchImages(RIETION + TwitterApiUtils.IMAGE_FILTER);
	}
	
	protected List<Message> searchRieMurakawaImages() {
		return searchImages(MURAKAWA_RIE + TwitterApiUtils.IMAGE_FILTER);
	}
	
	protected List<Message> searchImages(String searchKey) {
		SearchResults results = twitter.searchOperations().search(
				new SearchParameters(searchKey)
					.count(10));
		List<Tweet> tweets = results.getTweets();
		List<Message> messages = tweets.stream()
				.flatMap(tweet -> TwitterApiUtils.getImageUrlList(tweet).stream())
				.limit(3)
				.map(url -> new ImageMessage(url, url))
				.collect(Collectors.toList());
		if(messages.isEmpty()) {
			return Arrays.asList(new TextMessage("画像が見つかりませんでした"));
		} else {
			return messages;
		}
	}
	
	protected List<Message> searchRobotTexts() {
		return searchTexts(robotAccountId);
	}
	
	protected List<Message> searchRiemagicTexts() {
		return searchTexts(riemagicAccountId);
	}
	
	protected List<Message> searchTexts(String searchKey) {
		SearchResults results = twitter.searchOperations().search(
				new SearchParameters(searchKey)
					.count(1));
		List<Message> messages = results.getTweets().stream()
				.map(tweet -> new TextMessage(tweet.getText()))
				.collect(Collectors.toList());
		if(messages.isEmpty()) {
			return Arrays.asList(new TextMessage("ツイートが見つかりません"));
		} else {
			return messages;
		}
	}
	
}
```

メッセージに対するリプライもSpring MVCの機能ではなくSDKの機能を使います。
LineMessagingServiceをAutowiredして上記の要領でcallするだけでリプライの実装は終わりです。簡単ですね！
一つポイントですが、リプライには受信メッセージに含まれるReplyTokenを入れることが必須です。迷うことはないと思いますが。。

## ビルド・デプロイ

ここでLINEの管理画面に入ります。

![channel_secret.PNG](https://qiita-image-store.s3.amazonaws.com/0/91613/1a9f215d-3801-25e9-8671-b4b02ae8557f.png)

![channel_token.PNG](https://qiita-image-store.s3.amazonaws.com/0/91613/0d935272-87da-b715-a6d0-bc85bde10e63.png)

まずWebhook URLにエンドポイントを設定します。
そしてChannel SecretとChennel Access Tokenを確認します。今回自前で実装することはありませんでしたが、LINEからのメッセージは暗号化されているので、それの復号にSecretとTokenが使用されます。

SecretとTokenはそれぞれ、「line.bot.channelToken」「line.bot.channelSecret」にそれぞれ設定します。Spring Bootのプロパティの設定には例えば下記の方法があります。

* propertiesファイル
* ymlファイル
* システムプロパティ
* 環境変数

今回はHerokuのシステムプロパティに設定しました。


## 実行

実際のLINE画面は、肖像権の問題もありますので控えさせていただきます。。

なお、リプライ送信時に「IPをホワイトリストに登録してね」的なエラーになることがあります。
その場合は管理画面からServer IP Whitelistに追加する必要があります。
が、私の場合は1回登録して上手く行かず消したらリプライできるようになるという謎の現象がありました。。

# まとめ

LINE BOT SDKを使ってchat botをとても簡単に実装することができました。色々と面白い使いみちがありそうです。