SpringBoot(Kotlin)+nowでLINE Botを作ってみた

# はじめに
[LINE Advent Calendar 2017](https://engineering.linecorp.com/ja/blog/detail/210)の[Qiitaで人気のLINE関連投稿紹介（保存版）](https://engineering.linecorp.com/ja/blog/detail/234)を見て面白そうだったのでLINE Botを作ってみました。

私は普段はJavaを使っていて、SpringBootは趣味の範囲、Kotlinとnowは初心者です。そんな私がLINE BotをSpringBoot(Kotlin)とnowで開発してみてわかったことを中心に書いています。自分用の開発メモですが、誰かの役に立てば幸いです。

LINE Botの作り方を知りたい方は、[Qiitaで人気のLINE関連投稿紹介（保存版）](https://engineering.linecorp.com/ja/blog/detail/234)の「2. 作り方解説」で紹介されているブログ等をご参考ください。JavaやSpringBootでの開発はQiitaにいくつか記事があるので検索してみてください。

# 作ったBot
九九学習用のLINE Bot（DBや外部リソースへのアクセスは無し）
https://github.com/uemu/kukubot

九九の掛け算だけでなく、九九の次に習う割り算に向けて、九九の範囲で割り算も学習できます。

![screenshot](https://github.com/uemu/kukubot/raw/master/screenshot.png)

# 開発環境
* macOS Sierra 10.12.6
* [now](https://zeit.co/now)インストール済み
* LINE個人アカウント作成済み([LINE Developers](https://developers.line.me/ja/))
* IDEは[IntelliJ IDEA Community](https://www.jetbrains.com/idea/)

# 開発の流れ
1. LINE Messaging APIの登録
2. Botアプリの開発
3. Botアプリのデプロイ
4. LINE Messaging APIのWebhookの設定
5. 動作確認

# 1. LINE Messaging APIの登録
下記サイトで開発するBot用のChannelを作成します。丁寧に解説されているブログがありますので、本記事では説明を割愛します。

https://developers.line.me/ja/services/messaging-api/

# 2. Botアプリの開発
## Spring InitializrでGradleプロジェクトを作成
[https://start.spring.io/](https://start.spring.io/)でGradle Project、Kotlin、Spring Boot 1.5.9を選択し、Gradleプロジェクトを作成します。今回作ったBotは特にDependenciesを追加していません。

## build.gradleの追記

```build.gradle
// Spring InitializrでGradle、Kotlin、Spring Boot 1.5.9を選択して生成したbuild.gradleがベース
// 追加した部分はコメントに「追加: XXX」と記載
buildscript {
	ext {
		kotlinVersion = '1.2.0'
		springBootVersion = '1.5.9.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
		classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
	}
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'idea' // 追加: IntelliJ用
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'uemu'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
	kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
	kotlinOptions.jvmTarget = "1.8"
}

repositories {
	mavenCentral()
}


dependencies {
	compile('org.springframework.boot:spring-boot-starter')
	compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")
	compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
	compile('com.linecorp.bot:line-bot-spring-boot:1.12.0') // 追加: LINE Botのライブラリ
	testCompile('org.springframework.boot:spring-boot-starter-test')
}
```

## GradleプロジェクトをIntelliJにインポート
下記のようにgradleのideaタスクを実行してIntelliJ関連のファイルを作成し、その後にIntelliJでプロジェクトを開きます。

```
$ ./gradlew idea
```

## Botアプリの実装
https://github.com/line/line-bot-sdk-java/blob/master/line-bot-spring-boot/README.md
を参考に実装します。

`@LineMessageHandler`を付与したクラスを作成し、`@EventMapping`を付与したメソッドを実装します。

* handleTextMessageEventはテキストメッセージに対する処理を実装しています。複数のメッセージを返すので戻り値の型を`List<TextMessage>`にしています。
* handleDefaultMessageEventはテキスト以外のメッセージに対する処理を実装しています。何も処理せずメッセージも返さない実装になっています。

~~その他に`@PreDestroy`を付与したメソッドを実装しています。アプリケーションがスリープした際に利用者に通知します（Herokuでは動作しましたが、nowでは動作しませんでした、、、）。~~
LINE Messaging API（フリープラン）で動くように、`@PreDestroy`を付与したメソッドは削除しました。


```KukubotApplication.kt
package uemu.kukubot

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.message.TextMessage
import com.linecorp.bot.spring.boot.annotation.EventMapping
import com.linecorp.bot.spring.boot.annotation.LineMessageHandler
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import java.util.concurrent.ConcurrentHashMap

@SpringBootApplication
@LineMessageHandler
class KukubotApplication {

  private val userContextMap = ConcurrentHashMap<String, UserContext>()

  @EventMapping
  fun handleTextMessageEvent(messageEvent: MessageEvent<TextMessageContent>): List<TextMessage> {
    val context = userContextMap.getOrPut(messageEvent.source.userId) { UserContext() }
    val response = ArrayList<TextMessage>()
    val text = messageEvent.message.text

    if (text == "か") {
      context.mode = text
      context.fixedNumber = 0
      response.add(TextMessage("かけ算になったよ"))
    } else if (text == "わ") {
      context.mode = text
      context.fixedNumber = 0
      response.add(TextMessage("わり算になったよ"))
    } else if (text.matches(Regex("だ[1-9]"))) {
      context.fixedNumber = text.substring(1).toInt()
      response.add(TextMessage("${context.fixedNumber}のだんになったよ"))
    } else if (context.answer != 0 && text.matches(Regex("[0-9]+"))) {
      if (text.toInt() == context.answer) {
        context.combo++
        response.add(TextMessage("正かい！" + if (context.combo == 1) "" else " ${context.combo}れんぞく"))
      } else {
        context.combo = 0
        response.add(TextMessage("ざんねん ... 答えは${context.answer}"))
      }
    } else {
      response.add(TextMessage("[つかい方]\nか -> かけ算になるよ\nわ -> わり算になるよ\nだ数字 -> だんをえらべるよ\n数字 -> 答えられるよ"))
    }

    if (context.mode == "か") {
      context.firstNumber = if (context.fixedNumber == 0) (Math.random() * 9).toInt() + 1 else context.fixedNumber
      context.secondNumber = (Math.random() * 9).toInt() + 1
      context.answer = context.firstNumber * context.secondNumber
      response.add(TextMessage("${context.firstNumber} × ${context.secondNumber} = "))
    } else {
      context.answer = (Math.random() * 9).toInt() + 1
      context.secondNumber = if (context.fixedNumber == 0) (Math.random() * 9).toInt() + 1 else context.fixedNumber
      context.firstNumber = context.secondNumber * context.answer
      response.add(TextMessage("${context.firstNumber} ÷ ${context.secondNumber} = "))
    }

    return response
  }

  @EventMapping
  fun handleDefaultMessageEvent(event: Event) {
  }

}

class UserContext(var mode: String = "か",
                  var fixedNumber: Int = 0,
                  var firstNumber: Int = 0,
                  var secondNumber: Int = 0,
                  var answer: Int = 0,
                  var combo: Int = 0)

fun main(args: Array<String>) {
  SpringApplication.run(KukubotApplication::class.java, *args)
}
```

## 設定ファイルの作成
LINE Botが使用するchannelTokenとchannelSecretを環境変数から渡す前提で定義しています。

```application.properties
line.bot.channelToken=${CHANNEL_TOKEN}
line.bot.channelSecret=${CHANNEL_SECRET}
```

# 3. Botアプリのデプロイ
作成したアプリをnowにデプロイします。nowはDockerコンテナかNode.jsアプリか静的Webサイトをデプロイできます。SpringBootアプリなのでDockerコンテナとしてデプロイします。

https://zeit.co/now

nowを使ってみた感想ですが、Webサイトでユーザ登録などをせずにメールでの認証とコマンドラインのみでクラウドにアプリをデプロイできるので、とても便利です！

では、デプロイの準備として、下記の構成でnow作業用のディレクトリにいくつかファイルを作成します。

```
kukubot（Gradleプロジェクトのルートディレクトリ）
|--now
|  |--Dockerfile
|  |--deploy.sh
|  |--kukubot.jar // gradleのタスクで生成
|  |--now.json
|  |--undeploy.sh
```

## Dockerfileの作成

```Dockerfile
FROM java:8-jre-alpine
COPY kukubot.jar /
EXPOSE 8080
CMD ["java","-jar","/kukubot.jar"]
```

nowは内部でDockerfileで指定したポートに転送してくれるみたいです。

[Deploying Docker Apps](https://zeit.co/docs/deployment-types/docker)

## now.jsonの作成
アプリ名やアプリの別名や環境変数などを設定します。

[Configuring Now](https://zeit.co/docs/features/configuration)

```now.json
{
  "name": "kukubot",
  "alias": "kukubot",
  "env": {
    "CHANNEL_SECRET": "@channel-secret",
    "CHANNEL_TOKEN": "@channel-token"
  }
}
```

nameを指定することでデプロイしたアプリのURLが下記のようになります。

```
https://[name]-[ランダムな文字列].now.sh/
```

nowにデプロイするたびにURLのランダムな文字列が変わりますが、aliasを指定することでランダムな文字列を含まないURLでアプリにアクセスできるようになります。

```
https://[alias].now.sh/
```

OSSプランの場合はnowにデプロイしたアプリのソースが公開されます。公開したくない情報はnow secretで秘密にできます。

```
now secret add [key] [value]
```

秘密にした値は@[key]で使用できます。

[nowで、LineBotのチャンネルアクセストークンとチャンネルシークレットを隠蔽する](https://qiita.com/hirorin/items/2ff28306711fe19b0b33)
[Environment Variables and Secrets](https://zeit.co/docs/features/env-and-secrets)

## deploy.shとundeploy.shの作成
開発中に頻繁に実施するコマンドをスクリプト化しておきます。undeploy.shはちょっと強引かな。。。

```deploy.sh
#!/bin/bash

now && now alias
```

```undeploy.sh
#!/bin/bash

now rm $(now ls|grep "kukubot-"|cut -d" " -f2) //kukubotの部分はnow.jsonのnameに合わせる
```

## build.gradleの追記
~~テストに必要な環境変数や~~now作業ディレクトリにjarを作成するタスクを追記します。

```build.gradle

・・・

// 追加: nowにデプロイする前に実行するビルドタスク
task buildForNow(type: Copy, dependsOn: build) {
	from jar.archivePath
	into project.file('now')
	rename {
		'kukubot.jar'
	}
}

// 追加: cleanタスクの後にnow用のjarを削除
clean.doLast { //+
	project.file('now/kukubot.jar').delete()
}
```

## デプロイ
gradleのbuildForNowタスクを実行した後に、nowディレクトリに移動してdeploy.shを実行してデプロイします。

```
$ ./gradlew buildForNow
$ cd now
$ bash deploy.sh
```

# 4. LINE Messaging APIのWebhookの設定
「1. LINE Messaging APIの登録」で作成したChannelの基本設定でWebhook URLにデプロイしたアプリのURLを設定します。

```
https://[alias].now.sh/callback
```

接続確認が正常終了すれば設定完了です。

# 5. 動作確認
作成したBotをLINEの友達に登録し、動作確認を行います。
娘に使ってもらったところ、すぐに九九の範囲の割り算を覚えてしまい、作ったものの早くも役目を終えてしまいました。。。

![kukubot-check.png](https://qiita-image-store.s3.amazonaws.com/0/194566/ed0d684d-44bc-ec53-088c-4e86bf23ced9.png "kukubot-check.png")