はじめに
LINE Advent Calendar 2017のQiitaで人気のLINE関連投稿紹介(保存版)を見て面白そうだったのでLINE Botを作ってみました。
私は普段はJavaを使っていて、SpringBootは趣味の範囲、Kotlinとnowは初心者です。そんな私がLINE BotをSpringBoot(Kotlin)とnowで開発してみてわかったことを中心に書いています。自分用の開発メモですが、誰かの役に立てば幸いです。
LINE Botの作り方を知りたい方は、Qiitaで人気のLINE関連投稿紹介(保存版)の「2. 作り方解説」で紹介されているブログ等をご参考ください。JavaやSpringBootでの開発はQiitaにいくつか記事があるので検索してみてください。
作ったBot
九九学習用のLINE Bot(DBや外部リソースへのアクセスは無し)
https://github.com/uemu/kukubot
九九の掛け算だけでなく、九九の次に習う割り算に向けて、九九の範囲で割り算も学習できます。
開発環境
- macOS Sierra 10.12.6
- nowインストール済み
- LINE個人アカウント作成済み(LINE Developers)
- IDEはIntelliJ IDEA Community
開発の流れ
- LINE Messaging APIの登録
- Botアプリの開発
- Botアプリのデプロイ
- LINE Messaging APIのWebhookの設定
- 動作確認
1. LINE Messaging APIの登録
下記サイトで開発するBot用のChannelを作成します。丁寧に解説されているブログがありますので、本記事では説明を割愛します。
2. Botアプリの開発
Spring InitializrでGradleプロジェクトを作成
https://start.spring.io/でGradle Project、Kotlin、Spring Boot 1.5.9を選択し、Gradleプロジェクトを作成します。今回作ったBotは特にDependenciesを追加していません。
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
を付与したメソッドは削除しました。
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を環境変数から渡す前提で定義しています。
line.bot.channelToken=${CHANNEL_TOKEN}
line.bot.channelSecret=${CHANNEL_SECRET}
3. Botアプリのデプロイ
作成したアプリをnowにデプロイします。nowはDockerコンテナかNode.jsアプリか静的Webサイトをデプロイできます。SpringBootアプリなのでDockerコンテナとしてデプロイします。
nowを使ってみた感想ですが、Webサイトでユーザ登録などをせずにメールでの認証とコマンドラインのみでクラウドにアプリをデプロイできるので、とても便利です!
では、デプロイの準備として、下記の構成でnow作業用のディレクトリにいくつかファイルを作成します。
kukubot(Gradleプロジェクトのルートディレクトリ)
|--now
| |--Dockerfile
| |--deploy.sh
| |--kukubot.jar // gradleのタスクで生成
| |--now.json
| |--undeploy.sh
Dockerfileの作成
FROM java:8-jre-alpine
COPY kukubot.jar /
EXPOSE 8080
CMD ["java","-jar","/kukubot.jar"]
nowは内部でDockerfileで指定したポートに転送してくれるみたいです。
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のチャンネルアクセストークンとチャンネルシークレットを隠蔽する
Environment Variables and Secrets
deploy.shとundeploy.shの作成
開発中に頻繁に実施するコマンドをスクリプト化しておきます。undeploy.shはちょっと強引かな。。。
#!/bin/bash
now && now alias
#!/bin/bash
now rm $(now ls|grep "kukubot-"|cut -d" " -f2) //kukubotの部分はnow.jsonのnameに合わせる
build.gradleの追記
テストに必要な環境変数やnow作業ディレクトリにjarを作成するタスクを追記します。
・・・
// 追加: 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の友達に登録し、動作確認を行います。
娘に使ってもらったところ、すぐに九九の範囲の割り算を覚えてしまい、作ったものの早くも役目を終えてしまいました。。。