Kotlin/JSでLINE Botを動かす
Kotlin/JSを扱う題材としてCloud FunctionsでLINE Botを動かしてみたのですが、前回の記事がモリモリになってしまうので導入と実践を分けました。今回が実践編です。
前回の記事はこちら
環境は次のとおりです
題材
LINE Botを動かしてみることにしました。Slackもそうですが、inputとoutputが同じ場所で見えるチャット系サービスはCloud Functionsと相性が良いです。
Messaging API overview | LINE Developers
- ユーザーが、LINE公式アカウントにメッセージを送信します。
- LINEプラットフォームからボットサーバーのWebhook URLに、Webhookイベントが送信されます。
- Webhookイベントに応じて、ボットサーバーからユーザーにLINEプラットフォームを介して応答します。
今回は応答メッセージを使って、ユーザがLINE Botにメッセージを送ったときにオウム返しをするBotを作成します。
LINE Bot作成で必要なこと
LINE Botでは次のことが必要です
- リクエストからデータを受け取り
- 応答メッセージの作成
- LINEサーバにメッセージの送信
Cloud Functionsで動かす部分は同じです。
開発を便利にするためにいくつかの準備をします。
型の用意
せっかくKotlinで書くので型の恩恵を得られるようにします。
デプロイする関数の引数であるRequestとResponseの型を定義してあげます。今回利用する最低限度のものしか用意してないです。
- Wrapper.kt
external interface Request { val body: Any } external interface Response { fun status(code: Int): Response fun send(message: String): Response }
デプロイする関数を用意して、関数型として渡します。
- main.kt
import wrapper.Request import wrapper.Response external val exports: dynamic fun main() { exports.message = ::message } fun message(req: Request, res: Response) { TODO("実装を書く") }
これでエントリーポイントと実装が分けられてスッキリします。
kotlin-extentionsの導入
Kotlin/JSを扱うに当たり、ヘルパーが用意されてるので導入します。
@jetbrains/kotlin-extensions
という名前のnpmパッケージです。
- build.gradle.kts
dependencies { ... implementation(npm("@jetbrains/kotlin-extensions", "^1.0.1-pre.91")) ... }
npmパッケージが用意されてるので利用はできるのですが、デプロイを考えたときにちょっと問題があります。
生成されるJSファイルでは、require('kotlin-extensions')
となってしまうので利用時にエラーになります。
なので、JSファイルを生成する際に一部書き換えて対応します。
- build.gradle.kts
compileKotlinJs { doLast { // workaround for use kotlin-extensions val jsFile = File("build/js/packages/${project.name}/kotlin/${project.name}.js") val text = jsFile.readText() val rep = text.replace("require('kotlin-extensions')", "require('@jetbrains/kotlin-extensions')") jsFile.writeText(rep) } }
正直ちょっと手間ではあるので、利用したい部分のコードをローカルにコピーしてくる運用でも良いかもしれません。
環境変数の利用
トークンなどはソースコード管理に含めたくないので環境変数を経由して利用します。
js
関数でJSのコードを実行して利用します。
例えば
$ export CHANNEL_ACCESS_TOKEN=hogehoge
としていたら、Kotlinからは次のように利用できます。
js("process.env.CHANNEL_ACCESS_TOKEN") // hogehoge
型はdynamicです。
kotlinx.serializationの導入
通信の際にJSONデータを扱うので、手動でSerialize/deserializeしなくていいようにライブラリを利用します。
公式の通りにやれば追加できます。
注意として、バージョンは 0.14.0
と 0.20.0
しか対応していません。
package.jsonには kotlinx-serialization-kotlinx-serialization-runtime
というnpmパッケージで探しに行くので、次のnpmパッケージが一致して解決されます。
ちょっと気になるのは、このnpmパッケージが公式のものなのかどうかわからない点です。
npmパッケージの中身はbuild/package_imported
に生成されるJSファイルと同じなので気のせいかもしれませんが。
issueが閉じられてないのも気になります。
できればextentionと同じようにscopeを切って公式とわかるようにして、苦なく利用できるような形になっててほしいです。
通信ライブラリの導入
Axiosがdukatでいい感じに利用できたので、Axiosを採用します。
dukatについてはこちら
こんな感じでPOSTリクエストを送れます。HeaderにAuthorizationを追加してます。
- main.kt
private fun postReply(reply: Reply): Promise<JsonObject> { val axios = Axios.create(object : AxiosRequestConfig { override var method: String = "POST" override var responseType: String = "json" override var headers: Any? = kotlinext.js.js { this.Authorization = "Bearer ${js("process.env.CHANNEL_ACCESS_TOKEN")}" } as? Any }) return axios.post<Unit, JsonObject>(url = "https://api.line.me/v2/bot/message/reply", data = reply) }
kotlinext.js.js
は前述のkotlin-extentionsです。
JSON.stringifyでの循環参照回避
JSON.stringify(o: Any?)
を利用することで、JSのオブジェクトをJSON文字列に変換できます。
JVMの世界では起きなかったが、JSの世界では起きる問題があります。
stringifyしようとしたJSオブジェクトが、循環参照している場合例外が発生します。
それを解決するために第2引数に replacer: ((key: String, value: Any?) -> Any?)
という関数が用意されています。
こちらで循環参照してしまうものを除外して解決します。
次の例では、 request
というkeyが循環参照していたので除外しています。
- main.kt
fun message(req: Request, res: Response) { val webhookEvent = parseRequest(req) val reply = createReply(webhookEvent) postReply(reply) .then { console.log("then ${JSON.stringify(it) { key: String, value: Any? -> when (key) { "request" -> "" else -> value } }}") res.status(200).send("Success!!") }.catch { console.log("catch ${JSON.stringify(it)}") res.status(400).send("Error!!") } }
普段JSを書いていないので、慣れている人からしたら当たり前かもしれません。自分は困りました。
Cloud Functionsへのデプロイ
デプロイ時に環境変数を渡すのを忘れないようにします。
$ ./gradlew clean cKJ // compileKotlinJsの略 $ cd functions functions$ gcloud functions deploy message --region asia-northeast1 --trigger-http --runtime nodejs8 --update-env-vars CHANNEL_ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
まとめ
JSならではの部分や、エコシステム周りで足りない部分も感じますが、準備さえしてしまえばわりと解決できる問題が多いです。
Kotlin/JSでHello World以上のサンプルがまだ少ないので、いくらか参考になると思います。
READMEにLINE Botの友だち追加のQRを置いてあるので、気になった方は試すことはできます。
参考
Messaging API overview | LINE Developers