はじめに
久しぶりにLINE Botをつくってみました。今回はAWSでバックエンドの環境を構築してみましたが、AWS CDKを使ったのでAWSのコンソールを全く触らずにできました。
botの内容
こんな感じの「買い物リストbot」をつくりました。ここのgithubで公開しています。
このbotができることは、
- 買いたいものをどんどんリストに追加する
- 「リスト確認」で現在のリストを確認できる
- 「リストをクリア」でリストの中身を空にする
という単純なものです。このbotを作るにはデータを保管するストレージが必要になります。今回はDynamoDBを使いました。
開発準備
開発にあたって、ここのページを参考にさせていただきました。
あと、以下の自分用につくったnpmパッケージを使いました。
開発
CDKの構成
AWSの構成はこんな感じです(cdkだとコードだけでアーキテクチャを説明できるので楽ですね)。
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
というライブラリを呼ぶとLambda
とAPI Gateway
を一々呼ばなくても簡単にAPIで呼び出せるLambdaが作成できます。
また、ACCESS_TOKEN
とCHANNEL_SECRET
は.env
ファイルで用意します。このとき、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での設定は必要になりますが)。まずは対話モデルを作成してみました。
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
を実装します。
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
を実装します。
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リソースを使いたいときに簡単に呼び出せることだと思います。