🎉

Line Bot を Kotlin + Serverless Framework + AWS Lambda で作る

2023/11/18に公開

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 プログラミング言語 リンク

先にチャンネルを作っておく

まずはここでチェンネルを作りましょう。

https://developers.line.biz/ja/docs/messaging-api/getting-started/

チャンネル作成後、 Basic settings のタブで Channel secret を、Messaging API タブで Channel access token を取得してメモしておきましょう。

また Messaging API のタブを開き、Auto-reply-messagesGreeting messages を無効にしておきます。

実装していく

Kotlin のプロジェクトを立ち上げる

Gradle プロジェクトを作成して、 Bolt 関連と lambda 関連の依存ライブラリを build.gradlebuild.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 SecretChannel 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 と返信がくることが確認できれば完了です。

脚注
  1. リクエストヘッダー ↩︎

Discussion