Line Bot を Kotlin + Serverless Framework + AWS Lambda で作る
Line Bot を Kotlin + Serverless Framework + Lambda で作っていこうと思います。
使用技術
今回の使用技術は以下のようになっています。ツール系のインストールなどは完了しているとします。
技術 | 説明 | URL |
---|---|---|
LINE Messaging API SDK for Java | Line の Java用の SDK | リンク |
Amazon API Gateway + AWS Lambda | APIのエンドポイントおよびAPIの実装をここで | - |
Serverless Framework | サーバーレスアプリケーションの開発、デプロイ、管理ツール | リンク |
Kotlin | プログラミング言語 | リンク |
先にチャンネルを作っておく
まずはここでチェンネルを作りましょう。
チャンネル作成後、 Basic settings
のタブで Channel secret
を、Messaging API
タブで Channel access token
を取得してメモしておきましょう。
また Messaging API
のタブを開き、Auto-reply-messages
と Greeting messages
を無効にしておきます。
実装していく
Kotlin のプロジェクトを立ち上げる
Gradle プロジェクトを作成して、 Bolt 関連と lambda 関連の依存ライブラリを build.gradle
や build.gradle.kts
に追加します.
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.amazonaws:aws-lambda-java-core:1.2.1")
implementation("com.amazonaws:aws-java-sdk-lambda:1.11.907")
implementation("com.linecorp.bot:line-bot-messaging-api-client:8.0.0")
implementation("com.linecorp.bot:line-bot-parser:8.0.0")
implementation("com.linecorp.bot:line-bot-webhook:8.0.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
implementation("com.google.code.gson:gson:2.10.1")
}
まずは飛んでくるイベントを定義する
Line での イベント用のモデルとして com.linecorp.bot.webhook.model.Event
が定義されているのですが、実際には複数のイベントがリストになって飛んでくるようになっています。そのため、その型をまず用意しておきます。
import com.fasterxml.jackson.annotation.JsonInclude
import com.linecorp.bot.webhook.model.Event
@JsonInclude(JsonInclude.Include.NON_NULL)
data class LineEvents(
val events: List<Event>
)
RequestHandler を実装する
Lambda では RequestHandler
を実装する必要があります。
メッセージが来て、ただ単純に「Hello」と返すコードの全容は下記のようなコードになります。
class LineEventHandler : RequestHandler<APIGatewayProxyRequestEvent?, APIGatewayProxyResponseEvent> {
override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent {
val body = input?.body
val signature = input?.headers?.get("x-line-signature") ?: input?.headers?.get("X-Line-Signature")
if (body == null || signature == null) {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 400
this.body = "Invalid Request"
}
}
val validator = LineSignatureValidator(System.getenv("LINE_CHANNEL_SECRET").toByteArray())
if (!validator.validateSignature(body.toByteArray(), signature)) {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 403
this.body = "Forbidden"
}
}
try {
val objectMapper = ModelObjectMapper.createNewObjectMapper().registerKotlinModule()
val lineEvents = objectMapper.readValue(body, LineEvents::class.java)
val client = MessagingApiClient.builder(System.getenv("LINE_CHANNEL_ACCESS_TOKEN")).build()
val messageItems = lineEvents.events.forEach { event ->
if (event is MessageEvent) {
val message = event.message
if (message is TextMessageContent) {
client.replyMessage(
ReplyMessageRequest(
event.replyToken,
listOf(TextMessage("Hello")),
false
)
).join()
}
}
}
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 200
this.body = "Done"
}
} catch (t: Throwable) {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 500
this.body = "Internal Server Error"
}
}
}
}
handleRequest
の中を説明していきます。
1. 署名の検証をする
まずはこの部分です。
リクエストを受け取って、ヘッダーの x-line-signature
を受け取ります。 バージョンによっては X-Line-Signature
となっている場合もあるので、どちらでも受け取れるようにしておきます。 [1]
LineSignatureValidator
が用意されているので 環境変数LINE_CHANNEL_SECRET
をもとにバリデータのインスタンスを作成して、 x-line-signature
の値とbodyのバイト配列をバリデータに渡して、署名の検証を行います。
val body = input?.body
val signature = input?.headers?.get("x-line-signature") ?: input?.headers?.get("X-Line-Signature")
if (body == null || signature == null) {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 400
this.body = "Invalid Request"
}
}
val validator = LineSignatureValidator(System.getenv("LINE_CHANNEL_SECRET").toByteArray())
if (!validator.validateSignature(body.toByteArray(), signature)) {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = 403
this.body = "Forbidden"
}
}
2. body文字列を LineEvents に変換する
Line の SDK に ModelObjectMapper
が用意されているので、 body を渡して LineEvents のインスタンスに変換します。
val objectMapper = ModelObjectMapper.createNewObjectMapper().registerKotlinModule()
val lineEvents = objectMapper.readValue(body, LineEvents::class.java)
3. 返信する
環境変数 LINE_CHANNEL_ACCESS_TOKEN
をもとに MessagingApiClient
を作成して、 LineEvents
の中の Event
をもとに返信していきます。
今回は Hello
しか返さないので下記のようなコードになります。
val client = MessagingApiClient.builder(System.getenv("LINE_CHANNEL_ACCESS_TOKEN")).build()
val messageItems = lineEvents.events.forEach { event ->
if (event is MessageEvent) {
val message = event.message
if (message is TextMessageContent) {
client.replyMessage(
ReplyMessageRequest(
event.replyToken,
listOf(TextMessage("Hello")),
false
)
).join()
}
}
}
Serverless Frameworkを使って deploy までやる
.envを用意する
先ほどのChannel Secret
と Channel Access Token
をもとに .env
ファイルを作ります。
LINE_CHANNEL_SECRET=xxxxxxxxxxxx <-- 先ほどのChannel Secretを設定
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxxx <-- 先ほどのChannel Access Tokenを設定
serverless.yml を記述する
.env
ファイルと同じ場所に 次の serverless.yml
を作ります。
内容は下記で、適宜書き換えてください。
service: example-line-bot <--- lambda の名前を定義
useDotenv: true
provider:
name: aws
runtime: java17
stage: ${opt:stage, 'dev'}
region: ap-northeast-1
apiGateway:
shouldStartNameWithService: true
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
- lambda:InvokeAsync
Resource: "*"
environment:
SERVERLESS_STAGE: ${opt:stage, 'dev'}
LINE_CHANNEL_SECRET: ${env:LINE_CHANNEL_SECRET}
LINE_CHANNEL_ACCESS_TOKEN: ${env:LINE_CHANNEL_ACCESS_TOKEN}
package:
artifact: ./path/sample-line.jar <--- jarのパスを設定
functions:
api:
handler: com.xxx.LineEventHandler <--- 先ほど実装したクラスを設定
timeout: 30
events:
- http:
path: line/webhook
method: post
deployする
serverless.yml があるところで下記を実行します.
serverless deploy
デプロイが完了すると endpoint が表示されているはずなので、これをメモします。
流れちゃった人は、下記で再度 endpoint などを見ることができます。
serverless info
Webhook URL に登録する
先ほどメモした endpoint を Line 側に登録します。
動作確認
あとは Line で メンションを投げてみて Hello
と返信がくることが確認できれば完了です。
Discussion