LINE BOTをKotlin + SpringBoot + Herokuで作った

※2018/6/7 色々と加筆しました！

# LINE BOTをKotlin + SpringBoot + herokuで作る

LINE BOTに興味が出たので色々とググってたところ、
Kotlinでも作れるらしいことがわかったので作ってみました。
SpringBootを使うと楽らしかったので、今回初めて使ってみました。
KotlinはAndroid開発では使ったことがあるのですが、SpringBoot自体全くの経験なしでした。
ググってなんとか実装できました。

参考にした記事は以下になります。
https://qiita.com/uemu/items/38ba805c2453a8d68896
http://kikutaro777.hatenablog.com/entry/2017/01/16/230122
https://ledge.ai/line-messaging-api-exp/

ネット上にはHerokuを使用するにはFixieというアドオンを使う必要がある、という情報が多かったのですが、現時点では仕様が変わりアドオンは必要なくなったようです。

## 作りたいもの：野球BOT
プロ野球の観戦が趣味ということもあり、プロ野球関連の情報を教えてくれるBOTを作りたいと思っていました。

* 聞いたら選手の情報を教えてくれる
* 聞いたら当日の試合の情報を教えてくれる
* 試合の情報を定期的に通知してくれる

こんな機能のBOTが目標です。

## プランや機能など
LINE MessagingAPIにはプランが複数ありますが、無料で使用できるのは

* フリー
* Developer Trial

の2種類です。
Developer Trialは機能の制限は少なく、Push機能も使えますが友達登録が最大50人までという制限があります。
フリーは友達数の制限がない代わりにPush機能が使えないという制限があります。

※Push機能：BOTが自発的にユーザに対して発言を行う通知機能

どちらも一長一短ですが、全く制限がないプランの場合は月額2万円くらい取られてしまい、個人が趣味で運用するにはちょっと厳しいです。
当初から身内の野球好きで使えるBOTを作る予定で、Push機能は使いたかったので、友達数制限は気にせずDeveloper Trialを選択しました。

## 開発の流れ
おおまかには以下のような感じです。

1. [LINE Developers](https://developers.line.me/ja/)からLINE MessagingAPIの使用準備をする
2. [Spring Initializr](https://start.spring.io/)からSpring Bootプロジェクトの雛形を作成する
3. [IntelliJ IDEA](https://www.jetbrains.com/idea/download/)をインストールし、２で作成したプロジェクトをインポートする
4. [Heroku](https://jp.heroku.com/)アカウントを登録し、アプリを登録する
5. Herokuの環境変数に１で準備したChannel SecletとAccess Tokenを設定する（環境変数に設定しておけばアプリのプログラム内に生で書く必要がないので便利）
6. プログラムをガシガシ書いていく
7. アプリのプロジェクトとHerokuを連携し、Pushする
8. LINE DevelopersのBOT設定画面を開き、Webhook URLにHerokuのアプリのURLを設定し、接続確認する
9. 接続確認がOKなら、LINEでBOTを友達登録し、動作確認する

## LINE MessagingAPIをKotlinで使う時の基本
今回はGradle Projectで作っています。
build.gradleにLINE MessagingAPIの依存関係を追加します。

```xml:app/build.gradle
dependencies {
    ...
	compile('com.linecorp.bot:line-bot-spring-boot:1.12.0')
    ...
}
```

### 返信機能

```kotlin:WebhookController
@LineMessageHandler
class WebhookContoller {
    private lateinit var webhookService: WebhookService

    @EventMapping
    @Throws(Exception::class)
    fun handleTextMessageEvent(event: MessageEvent<TextMessageContent>): ArrayList<Message> {
        val messageList = ArrayList<Message>()
        println("event: {$event}")
        val message = event.message
        messageList.add(TextMessage(message.text))
        return messageList
    }
}
```

クラスの頭に@LineMessageHandlerというアノテーションをつけてあげるのを忘れないでください。
こんな感じのクラスを作ってあげると、BOTがユーザの投稿をそのままオウム返しするようになります。

```kotlin
fun handleTextMessageEvent(event: MessageEvent<TextMessageContent>): ArrayList<Message>{...}
```
このメソッドが「ユーザがメッセージを投稿した時のイベント」になります。
イベントはこれだけではなく、

・BOTを友達に追加
・BOTをグループに追加
・BOTをグループから退会

など色々あります。

メソッドの返り値をリストにしているのは、リストにしておくとメッセージ（BOTの吹き出し）を複数同時に返せるためです。
返せる吹き出しはイベント毎に同時に5つが最大のようです。（Pushは別）

event.message.textでユーザが投稿したメッセージを取得できるので、
これをif文やwhen文で条件分けすれば、ユーザが投稿した文章に対応する返信内容を作成することができます。

### Push（通知）機能
返信機能はサクッとつくれますが、Push機能は若干めんどくさいです。
まず前提として、通知を行うには送信先のIDが必要です。
IDにはユーザID、グループIDがあり、これらはユーザの投稿や友達追加、グループ追加などのイベント契機に簡単に取得できます。
通知を行うには、<b>これらIDをどこかに保持しておき、通知する度に取り出す必要があります。</b>

ではどこに保持しておけばええねんって話ですが、Herokuは無料プランでPostgresqlが使えるので、
私はそれをそのまま使っています。うーむ便利だ。

1. ユーザの投稿、友達追加、グループ追加イベントでユーザIDまたはグループIDを取得し、DBのテーブルにinsertしておく
2. 通知を行う際、必要に応じてDBからIDを取り出し、使用する

こんな感じです。

### IDの保持
コードにすると以下のようになります。

```kotlin:WebhookController
@LineMessageHandler
class WebhookContoller {
    private lateinit var webhookService: WebhookService

    @EventMapping
    @Throws(Exception::class)
    fun handleTextMessageEvent(event: MessageEvent<TextMessageContent>): ArrayList<Message> {
        val messageList = ArrayList<Message>()
        println("event: {$event}")

        // メッセージを受け取ったらsenderIDをDBにinsertしておく
        UserIdsDAO().insertUserId(event.source.senderId)

        val message = event.message
        messageList.add(TextMessage(message.text))
        return messageList
    }
}
```

DAOの内容は割愛します。
event.source.senderIdでユーザIDまたはグループIDが取得できます。
event.source.userIdという値も存在し、確かにこれでもユーザIDは取得できるのですが、
<b>投稿先がグループだった場合はエラーになります。</b>
ユーザとグループの両方に対応するためにも、senderIdにしておくのが無難でしょう。

### Push通知の実装
こちらをコードにすると以下のようになります。

```kotlin
fun pushMessage(lineMessagingClient: LineMessagingClient, msg: String) {
        val userIds = UserIdsDAO().findAllUserIds()
        for (userId in userIds) {
            try {
                lineMessagingClient.pushMessage(PushMessage(userId.getUserId(), TextMessage(msg))).get()
            } catch (e: Exception) {
                // 送信先ID消失によるエラーの可能性があるため、IDを削除したのちcontinueする
                println("エラー：$e")
                println(if (UserIdsDAO().deleteUserId(userId.getUserId().toString())) {
                    "UserIdを削除しました：${userId.getUserId()}"
                } else {
                    "UserIdの削除に失敗しました"
                })
                continue
            }
        }
    }
```

DBからユーザIDを全て取得し、for文で全てのユーザに通知するサンプルです。
もうちょっとスマートな実装方法がありそうですがどうなんでしょう...。

当然ですが、送信先IDが存在しない場合はエラーになるので、try catchで対策しています。
エラーが起きたらそのIDをテーブルから削除するようにしています。
エラーを起こさないためにも、ブロックイベントやグループ退会イベントなど、IDが消失するタイミングでしっかりとテーブルからもIDを削除しておくことが大事です。

あと個人的につまったところを
上記Push通知メソッドは引数にLineMessagingClientを持っているので、
使う際にはこいつを渡してあげる必要があります。
LineMessagingClientのインスタンス生成はこんな感じで書けます

```kotlin
val lineMessagingClient = LineMessagingClient.builder(System.getenv("LINE_BOT_CHANNEL_TOKEN")).build()
``` 

System.getenv("LINE_BOT_CHANNEL_TOKEN")の部分が重要で、これはHerokuの環境変数からLINE BOTのアクセストークンを取得しています。
なのでこの文字列のところは自分で設定したHerokuの環境変数名を指定してあげてください。
Herokuの環境変数の設定については下記の記事に詳しく書いてあったので参考にさせていただきました。

https://qiita.com/hirosat/items/39cd6ba954a451bc01b8

### 通知の契機
そんなこんなで、通知をする部分の実装はできましたが、今の状態だと通知を実行する契機がありません。
今回作りたかったのは、<b>毎日決まった時間に通知を行うBOT</b>なので、
定期実行の処理を入れてあげる必要があります。
その辺、SpringBootだと簡単に実装できてしまいます。

```kotlin:BotApplication
@EnableScheduling
@SpringBootApplication
class BotApplication {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(BotApplication::class.java, *args)
        }
    }
}
```

まず、定期実行をアプリで実装するために、アプリのメインクラスに@EnableSchedulingというアノテーションをつけてあげます。

```kotlin:ScheduledTaskService
/**
 * 定期実行する必要のあるメソッドをまとめたクラス
 * 基本的にPush投稿に使用する
 */
@Component
class ScheduledTaskService {
    /**
     * 毎日12~23時の間、一分ごとに試合開始時刻と現在時刻を比較し、
     * 一致した場合のみゲーム開始の通知を行う
     */
    @Scheduled(cron="0 */1 12-23 * * *", zone = "Asia/Tokyo")
    @Async
    fun executeScoringAlarm() {
        try {
            // 得点通知
            PushService().pushScoringAlarm()
        } catch (e: URISyntaxException) {
            println("Error:{$e}")
        }

        println("cron executed!")
    }
}
```
実際に作ったのがこちらです。
@Scheduled(cron="0 */1 12-23 * * *", zone = "Asia/Tokyo")
という部分で、どのようなタイミングで定期実行するかを指定しています。
普段はサーバサイドは触らないので、cronというものの書き方がいまいちわかりませんでいsたが、
これは<b>毎日１２時から２３時の間、〜時0分から1分毎に実行する</b>
という意味です。
cronの書き方については下記の記事を参考にしました。

https://qiita.com/rubytomato@github/items/4f0c64eb9a24eaceaa6e

メソッドで実行している内容はどうでもいいのですが、
「12時から23時の間にプロ野球の試合情報をネットから取得し、得点が追加されていた場合はユーザに通知する」
といった機能になっています。

そんなこんなでPushの実装もできました。

## Herokuを叩き起こす
試験的に運用していて気になったのが、しばらく放置したあとにBOTに話しかけると、最初の一回だけやたら返事が来るまで時間がかかる！という点でした。
調べると、どうもHerokuは無料プランの場合、15分間サーバにアクセスがないとスリープするらしい...とのことでした。
それでスリープ後の初回アクセス時に、起動というプロセスを挟むので時間が掛かってしまうようです。
通知の定期実行のタイミングで寝ていたら通知が実行されないのでは？と思い検索したところ、
やはり同じようなことで悩んでいる方々が色々と対策を乗せてくれていました。

簡単な話で、15分間アクセスがないとスリープするのであれば、15分毎にアクセスして眠らないようにしてやろう！ということです。とんでもないブラック案件ですね。
検索して得た情報の中で、一番簡単そうだったGoogleAppsScriptを使った実装にしています。

以下の記事を参考にさせていただきました。

https://qiita.com/uma0317/items/59af206f45b3a698d911

GoogleAppsScriptはGoogleアカウントがあれば簡単に利用できます。
これを使えば「15分毎にHerokuの該当APPにアクセスする」といったスクリプトを作成・実行できます。
ちなみに、アクセス先URLはHerokuの場合以下のようになります

```
http://[アプリ名].herokuapp.com/
```

必須ではありませんが、実装しておいた方がサクサク動くBOTになります。
実際、こちらのスクリプトの実装前と実装後では雲泥の差で使いやすくなりました。

### 

## 出来上がった野球BOT

* 野球の情報を教えてくれるBOT
* 基本的にjsoupによるスクレイピングでYahooから情報を取得してくる
* [@~]というコマンドを入力すると、それに対応したメッセージをくれる
* プロ野球の試合開始を毎日通知してくれる
* 試合中は得点が追加される度に通知してくれる

コマンドはこんな感じです

* @今日の試合：今日の試合予定や現在の得点を教えてくれる
* @昨日の試合：昨日の試合結果を教えてくれる
* @明日の試合：明日の試合予定を教えてくれる
* @現在時刻：現在時刻を教えてくれる（デバッグ用に入れてそのまま...
* @順位：セリーグとパリーグの順位を教えてくれる
* @@○○：○○には選手名が入ります。野球選手情報のURLを教えてくれます
* @応援歌@□@○○：□には球団名頭文字（巨人なら巨）、○○には選手名が入ります。選手応援歌の歌詞を教えてくれます
* @停止：BOTが何も喋らなくなります
* @再開：喋らなくなったBOTが喋るようになります
* @通知オン 全：全チームの通知がオンになります（初期は全チームオンです）
* @通知オン 球団名：指定したチームの通知をオンにします
* @通知オフ 全:全チームの通知がオフになります
* @通知オフ 球団名：指定したチームの通知をオフにします

結構もりもりです。
スクリーンショットを乗せておくのでよかったらどうぞ。

## BOTスクリーンショット

### 試合日程確認
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/1adda402-43c8-d545-1f7c-770060e7d348.jpeg" width="60%">

<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/16128d8a-fdfc-c0c8-30a9-e9ce67a7e88b.jpeg" width="60%">

### 停止と再開
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/ba5d77d5-66ea-2ca4-fd5e-b53df746ad78.jpeg" width="60%">

<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/4d65ca83-4b28-b528-6774-429a5d39cb23.jpeg" width="60%">


### 通知オンオフ
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/e8f130a6-647a-4fe3-d6c6-08b74ab12336.jpeg" width="60%">

### 試合開始通知
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/8489adfe-b029-f6bf-44d7-4e1b0dadb447.jpeg" width="60%">

### 得点通知
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/6deefb45-efbc-b9d2-9555-827a0853cf88.jpeg" width="60%">

### 応援歌歌詞検索
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/b2da9ff6-0c29-ea32-4655-04bd16ad60ca.jpeg" width="60%">

### 選手検索
<img src="https://qiita-image-store.s3.amazonaws.com/0/154882/087aacdd-f365-2c57-a06a-a02b0c961eb4.jpeg" width="60%">

## まとめ
今回は完全無料でLINE BOTというものを作ってみましたが、結構簡単に作れましたし、
何よりアイデア次第でなんでも作れそうだということがわかりました。
Androidアプリ以外でもKotlinを実用的に使えたのは収穫です。
みなさんもぜひLINE BOTを作ってみてください。

ちなみに、冒頭でも触れましたが、このBOTは身内で楽しむ用に作ったため、DeveloperTrialプランで作成しています。
なので友達登録の最大人数が５０名となっているため、公開予定はありません

仮に公開したところで50人の人が登録してくれるかどうかもわからないですが！

以上です！ありがとうございました！