2020年7月、我が家に長男が誕生。
もう天使。かわいい。CMのオファー来るんじゃないのか?(親バカ)
親族・友人に息子を会わせて、可愛さを自慢やりたかったが、このご時世それも叶わず。。。
我が息子の可愛さを普及したい。どうしよう。
そうだ。我が息子の可愛さを普及するLINE Botを作ろう。
1. デモ
2. 全体構成
2-1. LINEBotからWebHookでAPIGateway→Lambda実行
2-2. メッセージを解析して、返信用のメッセージと画像を取得
2-3. LINEに返信
とシンプルなもの。
GASとかで構築した方が安い。というのは秘密。
3. LINE①
まずはLINEBot MessagingAPIの設定。
下記サイトを参考に設定。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest
Webhook URLはAPI構築後に設定。
4. AWS
Lambda + DynamoDBはCloudFormationで、APIGateway + S3は手動で構築。
4-1. CloudFormation
AWSTemplateFormatVersion: 2010-09-09
Resources:
HaruBotFunction:
Type: AWS::Lambda::Function
Properties:
Code: ./release/app.zip
FunctionName: HaruBotFunction
Handler: index.handler
Runtime: nodejs12.x
Role:
Fn::GetAtt:
- "HaruBotRole"
- "Arn"
MemorySize: 128
Timeout: 30
Environment:
Variables:
TZ: Asia/Tokyo
ACCESSTOKEN: <LINE ACCESSTOKEN>
CHANNELSECRET: <LINE CHANNELSECRET>
ReplyMapping:
Type: AWS::DynamoDB::Table
Properties:
TableName: ReplyMapping
AttributeDefinitions:
- AttributeName: type
AttributeType: S
KeySchema:
- AttributeName: type
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 3
WriteCapacityUnits: 3
HaruBotRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- "edgelambda.amazonaws.com"
- "lambda.amazonaws.com"
- "dynamodb.amazonaws.com"
- "cloudwatch.amazonaws.com"
Action:
- sts:AssumeRole
- ACCESSTOKEN, CHANNELSECRETはLINE Bot設定時に発行した値を設定。
- ReadCapacityUnits, WriteCapacityUnitsは適当に。一応複数人に公開することを考慮して3。
- Roleは最低限の権限のみ付与。
4-2. DynamoDB
4-2-1. テーブルデザイン
Key | 型 | 説明 |
---|---|---|
type | String | PK |
img | List | 画像URLのリスト、今回はS3上に静的WebホスティングしたURLを設定 |
msg | List | 返信メッセージのリスト |
4-2-2. サンプル
{
"img": [
"https://hogehoge.com/oyasumi01.jpewg",
"https://hogehoge.com/oyasumi02.jpewg"
],
"msg": [
"おやすみなちゃい",
"Zzzz..."
],
"type": "おやすみ"
}
4-3. Lambda
4-3-1. ソース全文
'use strict'
const line = require('@line/bot-sdk')
const client = new line.Client({ channelAccessToken: process.env.ACCESSTOKEN })
const crypto = require('crypto')
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB.DocumentClient({
region: 'ap-northeast-1'
})
exports.handler = async (event) => {
return new Promise(async (resolve, reject) => {
const signature = crypto.createHmac('sha256', process.env.CHANNELSECRET).update(event.body).digest('base64')
const checkHeader = (event.headers || {})['X-Line-Signature']
if (signature !== checkHeader) {
reject('Authentication error')
}
const body = JSON.parse(event.body)
const events = body.events[0]
const message = events.message.text
const param = {
TableName: 'ReplyMapping',
Key: {
type: message
}
}
const result = await dynamodb.get(param).promise()
let msg, img
console.log(result.Item)
if (result.Item) {
const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
msg = getRandomList(result.Item.msg)
img = getRandomList(result.Item.img)
} else {
msg = `${message}ってなんでちゅか?`
img = ''
}
const replyText = {
type: 'text',
text: msg
}
const replyImage = {
type: 'image',
originalContentUrl: img,
previewImageUrl: img
}
client.replyMessage(events.replyToken, replyText)
.then((r) => {
return client.pushMessage(events.source.userId, replyImage)
})
.then((r) => {
resolve({
statusCode: 200,
headers: { 'X-Line-Status': 'OK' },
body: '{"result":"completed"}'
})
}).catch(reject)
})
}
4-3-2. 認証処理
const signature = crypto.createHmac('sha256', process.env.CHANNELSECRET).update(event.body).digest('base64')
const checkHeader = (event.headers || {})['X-Line-Signature']
if (signature !== checkHeader) {
reject('Authentication error')
}
LINEからハッシュ化されている値と突合して、正しいリクエストかどうかハンドリング。
4-3-3. 返信用のメッセージ、画像を取得
const body = JSON.parse(event.body)
const events = body.events[0]
const message = events.message.text
const param = {
TableName: 'ReplyMapping',
Key: {
type: message
}
}
const result = await dynamodb.get(param).promise()
let msg, img
if (result.Item) {
const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
msg = getRandomList(result.Item.msg)
img = getRandomList(result.Item.img)
} else {
msg = `${message}ってなんでちゅか?`
img = ''
}
送信されたメッセージを元に、DynamoDBからレコード取得。
※ ユーザーからの送信テキストををKeyに完全一致でレコードを取得しているため、良さげなワードから良さげに引っ張ってくることはできない。
例えば、「かわいい」のみレコードを登録していた場合、「かわいいね」とメッセージが送られてきても、良さげな返信はされない。
形態素解析して、良さげな言葉を引っ張ってきたかったが、時間が足りず。。(お宮参りに間に合わせたかったんだもん。)
レコードが存在する場合は、レコード内のimg, msgからランダムに値を取得。
存在しない場合はデフォルトを返却。
4-3-4. 返信
const replyText = {
type: 'text',
text: msg
}
const replyImage = {
type: 'image',
originalContentUrl: img,
previewImageUrl: img
}
client.replyMessage(events.replyToken, replyText)
.then((r) => {
return client.pushMessage(events.source.userId, replyImage)
})
.then((r) => {
resolve({
statusCode: 200,
headers: { 'X-Line-Status': 'OK' },
body: '{"result":"completed"}'
})
}).catch(reject)
詳しいことはドキュメントを参考に。
4-4. APIGateway
RestAPIで作成。
公開するAPIではないので、特にカスタムドメイン名の設定はしない。
4-4-1. APIGateway > API作成押下
4-4-2. REST API > 構築
4-4-3. API名入力 > APIの作成
4-4-4. メソッドの作成 > post選択
4-4-5. Lambda関数選択 > 保存
Lambda関数はCloudFormationmで構築したLambda関数を定義。
今回は HaruBotFunction
4-4-6. メソッドリクエスト > HTTPリクエストヘッダー追加
設定する値。
名前 | 必須 | キャッシュ |
---|---|---|
X-Line-Signature | T | F |
4-4-7. APIのデプロイ > URLコピー
5. LINE②
4-4-7. でコピーしたURLを、LINE Developers > Messaging API設定 > Webhook設定 のWebhook URLに登録。
6. 完成
これで完成。
DynamoDBに会話用のレコードを登録し、CDNに画像をアップロードすればデモのように動かせることができる。
7. まとめ
7-1. 残念なロジック
今回は、息子に直接会えない親族や、友人に少しでも息子に触れ合ってもらえるようにBotを構築。
お宮参り後に公開したくてちょっと無理やりな構成になってしまったのは残念。
特に、形態素解析して、ワードをピックアップするロジックを組みたかったけど、うまくいかなかったため、残念なロジックに・・・
ここは改善したい。
ゆくゆくは、メンテナンス不要で、いい感じに更新されていくような構築ができたら完璧。
7-2. CDNをS3にする必要は..
家族アルバムアプリのみてねっていい感じのAPIないかなぁ?
日時指定すると、いい感じの画像を返却してくれるAPIが欲しい。笑
もしあれば課金しますわ。
というか、Instagramでもいいかなぁ。(怒られる恐れあり?)