4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CDKで「買い物リスト」LINE botをつくる

Posted at

はじめに

久しぶりにLINE Botをつくってみました。今回はAWSでバックエンドの環境を構築してみましたが、AWS CDKを使ったのでAWSのコンソールを全く触らずにできました。

botの内容

こんな感じの「買い物リストbot」をつくりました。ここのgithubで公開しています

clear.png

このbotができることは、

  1. 買いたいものをどんどんリストに追加する
  2. 「リスト確認」で現在のリストを確認できる
  3. 「リストをクリア」でリストの中身を空にする

という単純なものです。このbotを作るにはデータを保管するストレージが必要になります。今回はDynamoDBを使いました。

開発準備

開発にあたって、ここのページを参考にさせていただきました。
あと、以下の自分用につくったnpmパッケージを使いました。

開発

CDKの構成

AWSの構成はこんな感じです(cdkだとコードだけでアーキテクチャを説明できるので楽ですね)。

/lib/cdk-line-bot-stack.ts
import * as cdk from '@aws-cdk/core'
import { LambdaApi } from 'cdk-lambda-api'
import * as dynamodb from '@aws-cdk/aws-dynamodb'

export class CdkLineBotStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const table = new dynamodb.Table(this, 'Table', {
      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
    })

    const lambdaApi = new LambdaApi(this, 'LineBot', {
      lambdaPath: 'Linebot',
      environment: {
        ACCESS_TOKEN: process.env.ACCESS_TOKEN!,
        CHANNEL_SECRET: process.env.CHANNEL_SECRET!,
        TABLE_NAME: table.tableName,
      }
    })

    table.grantFullAccess(lambdaApi.handler)
  }
}

LambdaApiというライブラリを呼ぶとLambdaAPI Gatewayを一々呼ばなくても簡単にAPIで呼び出せるLambdaが作成できます。
また、ACCESS_TOKENCHANNEL_SECRET.envファイルで用意します。このとき、cdk.jsonを以下のように書き換えます。

cdk.json
{
  "app": "node -r dotenv/config -r ts-node/register bin/cdk-line-bot.ts"
}

あと、dotenvをインストールしておきます。

yarn add dotenv

lambdaの実装

これでAWSの構成は定義できたので、lambdaを書いていけばいつもどおりにLINE botが作成できます(当然、LINE Developersでの設定は必要になりますが)。まずは対話モデルを作成してみました。

/linebot/conversationModel.ts
export enum MessageType {
    Confirm,
    Clear,
    Add,
}

interface ReplyModel {
    message: string,
    type: MessageType,
}

export const conversation = (message: string): ReplyModel => {
    switch (message) {
        case 'リスト確認':
            return { message: '現在の買い物リストです。', type: MessageType.Confirm }
        case 'リストをクリア':
            return { message: 'リストをクリアしました', type: MessageType.Clear }
        default:
            return { message: `${message}をリストに追加します。`, type: MessageType.Add }
    }
}

拾い上げるワードは「リスト確認」と「リストをクリア」だけ拾い上げてあとは「買い物リストに追加したいもの」とみなします。
あとは、DynamoDBの操作をするためのdbHandlerを実装します。

/linebot/dbHandler.ts
import * as AWS from 'aws-sdk'
import { MessageType } from './conversationModel'

const dynamodb = new AWS.DynamoDB()

async function addList(message: string, userId: string): Promise<string[]> {
  const { Item } = await dynamodb.getItem({
    TableName: process.env.TABLE_NAME!,
    Key: {
      userId: {
        S: userId
      }
    },
  }).promise()
  const shoppingList: AWS.DynamoDB.AttributeValue[] = [{ S: message }]
  if (Item) {
    Item.shoppingList.L?.map(value=>shoppingList.push(value))
  }
  await dynamodb.putItem({
    TableName: process.env.TABLE_NAME!,
    Item: {
      userId: {
        S: userId
      },
      shoppingList: {
        L: shoppingList
      }
    }
  }).promise()
  return []
}

async function confirmList(userId: string): Promise<string[]> {
  const { Item } = await dynamodb.getItem({
    TableName: process.env.TABLE_NAME!,
    Key: {
      userId: {
        S: userId
      }
    },
  }).promise()
  const shoppingList: string[] = []
  if (Item) {
    Item.shoppingList.L?.map(value=>shoppingList.push(value.S!))
  }
  return shoppingList
}

async function clearList(userId: string): Promise<string[]> {
  await dynamodb.deleteItem({
    TableName: process.env.TABLE_NAME!,
    Key: {
      userId: {
        S: userId
      }
    },
  }).promise()
  return[]
}

export async function dbHandler(messageType: MessageType, message: string, userId: string): Promise<string[]> {
  switch(messageType) {
    case MessageType.Add:
      return await addList(message, userId)
    case MessageType.Confirm:
      return await confirmList(userId)
    case MessageType.Clear:
      return await clearList(userId)
  }
}

ここらへんの実装自分でもいまいちだと思っているので指摘あればGitHubの方にPRください!
最後にこれらを呼び出すindex.handlerを実装します。

/linebot/index.ts
import * as Lambda from 'aws-lambda'
import * as Line from '@line/bot-sdk'
import * as Types from '@line/bot-sdk/lib/types'
import { buildReplyText } from 'line-message-builder'
import { conversation } from './conversationModel'
import { dbHandler } from './dbHandler'

const channelAccessToken = process.env.ACCESS_TOKEN!
const channelSecret = process.env.CHANNEL_SECRET!

const config: Line.ClientConfig = {
    channelAccessToken,
    channelSecret,
}
const client = new Line.Client(config)

async function eventHandler(event: Types.MessageEvent): Promise<any> {
    if (event.type !== 'message' || event.message.type !== 'text' || !event.source.userId) {
        return null
    }
    const message = conversation(event.message.text)

    const replyText = [message.message]

    const shoppingList = await dbHandler(message.type, event.message.text, event.source.userId)
    if (shoppingList.length > 0) {
        shoppingList.map(value => replyText.push(value))
    }
    return client.replyMessage(event.replyToken, buildReplyText(replyText))
}

export const handler: Lambda.APIGatewayProxyHandler = async (proxyEevent: Lambda.APIGatewayEvent, _context) => {
    console.log(JSON.stringify(proxyEevent))

    const signature = proxyEevent.headers['X-Line-Signature']
    if (!Line.validateSignature(proxyEevent.body!, channelSecret, signature)) {
        throw new Line.SignatureValidationFailed('signature validation failed', signature)
    }

    const body: Line.WebhookRequestBody = JSON.parse(proxyEevent.body!)
    await Promise
        .all(body.events.map(async event => eventHandler(event as Types.MessageEvent)))
        .catch(err => {
            console.error(err.Message)
            return {
                statusCode: 500,
                body: 'Error'
            }
        })
    return {
        statusCode: 200,
        body: 'OK'
    }
}

さきほどLambdaApiで渡した環境変数をprocess.env.hogehogeで呼び出すことができます。

さいごに

2,3時間程度で作った(設計もまともにしていない)botなのでシステムとしては荒いですが、今回でcdkで簡単にLINE botが作れることがわかりました。AWS CDKを使う利点は、API GatewayとLambda以外のAWSリソースを使いたいときに簡単に呼び出せることだと思います。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?