使用 AWS Lambda 部署 Line 聊天機器人 (下)
前言
在上一篇 Blog 中,我們教大家怎麼使用 AWS Lambda 建置一個 Line Bot 的後端做出一個 Echo Bot,離健康回報機器人又更進一步了呢!
開始文章之前,也歡迎一起打開 Podcast,跟著講師的說明一起看看這篇文章。
今天這篇 Blog 就要來整合 AWS 其他服務:
- DynamoDB
- CloudWatch
讓我們達到紀錄 User 回報狀況及提醒 User 要回報等進階邏輯。
情境
在本實驗中,我們會利用 LineBot 抓取 “Done”、“填” 等關鍵字,將其回報紀錄寫入 DynamoDB 中。在每天早上 8:45 及下午 5:45 將尚未填寫健康回報表單的人條列出來,並推送至 Line 群組給大家知道,達到互相提醒的效果。
在 AWS Lambda 中使用 DynamoDB
在上一篇 Blog 中,我們已經順利讓 Line Bot 和 AWS Lambda 連動了,但我們的應用場景是用這隻機器人來幫我們紀錄健康回報狀況,所以這邊我們會需要一個資料庫來儲存回報訊息,我們還是選擇無伺服器的 DynamoDB 來實作,這樣既可以保持整體架構的無伺服器化,也因為我們需求還沒明確,使用非關聯式的資料庫確保我們的資料結構定義上可以更加彈性。
-
AWS Lambda 對 DynamoDB 的存取權限
AWS Lambda 沒有 DynamoDB 時卻要將資料放入(put)進 DynamoDB 會報錯
我們就要針對該 role 去調整權限,加入 AmazonDynamoDBFullAccess 即可
-
程式邏輯設計
我們需要紀錄要被追蹤的人,並且每次的彙報紀錄需要被存檔,因此我這邊簡單設計需要 2 張資料表,分別為:
- 使用者紀錄(HealthReportUser)
將唯一的 Line ID 設成 Key
記錄 User 的名字
- 使用者紀錄(HealthReportUser)
* 回報紀錄(HealthReportRecord)
將唯一的 Line ID 及回報時間設成 Key
指令設計
* reg \<username\>:綁定 Line UserID 與 User name,以便後續追蹤回報狀況
* dereg:解除綁定
* done、填:回報健康紀錄
直接附上程式碼
```python
from linebot import (
LineBotApi, WebhookHandler
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage,
)
from datetime import datetime, timedelta
import os
import json
import boto3
from boto3.dynamodb.conditions import Key
line_bot_api = LineBotApi(os.environ['Channel_access_token'])
handler = WebhookHandler(os.environ['Channel_secret'])
tz = timedelta(hours = 8)
def lambda_handler(event, context):
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
dynamodb = boto3.resource('dynamodb') # connect to DynamoDB
HealthReportUser = dynamodb.Table('HealthReportUser') # select table we will use after.
HealthReportRecord = dynamodb.Table('HealthReportRecord')# select table we will use after.
ori_text = event.message.text
texts = ori_text.split()
if len(texts) > 0 :
cmd = texts.pop(0).lower()
if cmd == "reg": # command parser
if len(texts) > 0 :
user = {
'LineID': event.source.user_id,
'UserName': texts[0]
}
HealthReportUser.put_item(Item = user) # insert data into DynamoDB
elif cmd == "dereg":
user = {
'LineID': event.source.user_id
}
HealthReportUser = dynamodb.Table('HealthCare')
HealthReportUser.delete_item(Key = user) # delete data into DynamoDB
elif cmd == "done":
response = HealthReportUser.query(
KeyConditionExpression=
Key('LineID').eq(event.source.user_id)
) # select data from DynamoDB
user = response.get('Items')
if user != None:
if len(user) != 0:
user = user[0]
now = datetime.now() + tz
morning_start = int(datetime.timestamp(datetime(now.year, now.month, now.day, 6, 0, 0, 0)))
morning_end = int(datetime.timestamp(datetime(now.year, now.month, now.day, 9, 0, 0, 0)))
evening_start = int(datetime.timestamp(datetime(now.year, now.month, now.day, 17, 0, 0, 0)))
evening_end = int(datetime.timestamp(datetime(now.year, now.month, now.day, 18, 0, 0, 0)))
now = datetime.timestamp(now)
if (now > morning_start and now < morning_end): # check date time in reporting area
response = HealthReportRecord.query(
KeyConditionExpression=
Key('LineID').eq(event.source.user_id) & Key('RecordTime').between(morning_start, morning_end)
)
logs = response.get('Items')
if logs != None:
if len(logs) == 0:
racord = {
'LineID': event.source.user_id,
'RecordTime': int(now)
}
HealthReportRecord.put_item(Item = racord)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = user['UserName'] + ' morning report successfully.'))
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = user['UserName'] + ' already report.'))
elif(now > evening_start and now < evening_end):
response = HealthReportRecord.query(
KeyConditionExpression=
Key('LineID').eq(event.source.user_id) & Key('RecordTime').between(evening_start, evening_end)
)
logs = response.get('Items')
if logs != None:
if len(logs) == 0:
racord = {
'LineID': event.source.user_id,
'LogDateTime': int(now)
}
HealthReportRecord.put_item(Item = racord)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = user['UserName'] + ' evening report successfully.'))
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = user['UserName'] + ' already report.'))
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = user['UserName'] + ' not in report time.'))
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="Please reg first. With command reg <yourname>"))
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="Please reg first. With command reg <yourname>"))
# get X-Line-Signature header value
signature = event['headers']['x-line-signature']
# get request body as text
body = event['body']
# handle webhook body
try:
handler.handle(body, signature)
except InvalidSignatureError:
return {
'statusCode': 502,
'body': json.dumps("Invalid signature. Please check your channel access token/channel secret.")
}
return {
'statusCode': 200,
'body': json.dumps("Hello from Lambda!")
}
```
在重新部署上去即可在機器人聊天室註冊或回報健康表單填寫狀態。
利用 Amazon CloudWatch 的 cron job 設定定時提醒功能
在完成回報機器人之後,我們要把每天早晚回報的結果推送至群組以利追蹤,這時就可以依靠
Amazon CloudWatch 中的 Rules 達到目的,設定方法也很簡單!
進到 Amazon CloudWatch 的後台,並選擇左邊選單的 Rules
選擇 Create Rule
選擇 Schedule、Cron expression,輸入
45 0 ? * MON-FRI *
GMT 凌晨 12:45 -> 8:45 GMT + 8
並選擇要觸發的 AWS Lambda 函數,即可按下 Configure details
給一個名字即可,就可以 Create 了
附上 Lambda 函數程式碼
from linebot import (
LineBotApi, WebhookHandler
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage,
)
from datetime import datetime, timedelta
import json
import logging
import os
logger = logging.getLogger()
logger.setLevel(logging.ERROR)
import boto3
from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key
line_bot_api = LineBotApi(os.environ['Channel_access_token'])
handler = WebhookHandler(os.environ['Channel_secret'])
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
now = datetime.now()
morning_start = int(datetime.timestamp(datetime(now.year, now.month, now.day, 6, 0, 0, 0)))
morning_end = int(datetime.timestamp(datetime(now.year, now.month, now.day, 9, 0, 0, 0)))
print(morning_start, morning_end)
HealthRacordMorning = dynamodb.Table('HealthReportRecord')
HealthReportUser = dynamodb.Table('HealthReportUser')
all_users = HealthReportUser.scan()
all_users = all_users.get('Items')
morning_miss = []
for user in all_users:
morning_record = HealthRacordMorning.query(
KeyConditionExpression=
Key('LineID').eq(user['LineID']) & Key('RecordTime').between(morning_start, morning_end)
)
morning_record = morning_record.get('Items')
if len(morning_record) == 0:
morning_miss.append(user['UserName'])
remind_str = ""
for user_name in morning_miss:
remind_str += user_name + ", "
if remind_str != "":
line_bot_api.push_message(os.environ['Group_Id'], TextSendMessage(text = remind_str + "not reply in morning."))
> os.environ['Group_Id'] 為需要推送的群組 ID
結果呈現如下圖:
在每天早上的 8:45 跟下午的 5:45 ,機器人會告訴群組內的人誰還沒回報。
結論
原本此機器人預計放到群組中給大家使用的,但因為 Line 的免費方案只能讓訊息有 500 次的已讀量,因此後來有改版成利用 Line notify 來做早晚提醒 User 的功能,以達到 cost effective 的最佳實踐。
本篇內容將學到
- 在 AWS Lambda Function 中使用 DynamoDB
- 利用 Amazon CloudWatch 的 rule 產生 cron job 觸發 AWS Lambda Function