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