卡尼多隨筆

認識自我 • 感受世界 • 創造價值

0%

黑客松 LINE Bot 賽前補帖

簡單和大家分享適合搭配黑客松 LINE Bot 的相關技術~

2021 升大四的暑假 (7/1~7/3),和 Justin、Frank、Jennie、Henry 參加了由交大國際事務處主辦的「NYCU Global Design & Innovation Competition」黑客松競賽,有不少收穫!
(這場黑客松很特別,因為疫情的關係,原本是實體的,改採線上競賽)


我那場黑客松

題目

How to use Open Data combined with LINE Bot to improve medical and health care
(以 LINE 的聊天機器人結合 Open Data,在醫療、照護、健康領域進行創意發想)

Presentation Video

https://www.youtube.com/watch?v=djUoMO8IMcU

System Design

  • 我們用 Flask 後端及 FSM model 當作 LINE Bot 的基本架構
    • 之所以選擇用 FSM model 而不是簡單的 if else 是因為,我們認為和 Bot 對話的情境就像是一個 FSM,所以用 FSM model 去描述再適合不過了!
  • 用 Firebase Firestore 做資料庫的讀寫
  • 用 Firebase Storage 做圖片的上傳及 URL 生成
  • 用 Heroku 部署 LINE Bot server
  • 用 Algorithmia 平台部署我們的 ML model 和一個 function
    • 為何不和 LINE Bot server 一起部署?因為這麼做不但能減輕 server 的負擔,也不必安裝 ML 相關的套件,減少空間的使用
  • 用 GitHub Pages 部署 LIFF 頁面

FSM 狀態圖

  • 在開始打 code 前,我們用 FSM 狀態圖設計了整個 project 功能的架構
  • 有了這個狀態圖,掌握整個 project 的輪廓,打 code 的過程就比較不會那麼雜亂!

實用技術分享

LINE Bot 本體

可以參考我「用 FSM 及 LINE Messaging API 做一個聊天機器人」這一篇文。
我將會帶大家用 LINE DevelopersPipenvFSM (有限狀態機) 的概念,做一個簡單的 LINE Bot。
並用 Herokungrok 部署程式碼以測試我們的 LINE Bot!

Messaging API

待會我會簡單帶一下 quick reply、flex message、carousel template、rich menu,
其他還有很多很酷的功能詳見官方文檔

Quick Reply

官方文檔:Using quick replies

你可能會很好奇,該怎麼把官方文檔上的 JSON 轉換成 Python code 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"type": "text",
"text": "Select your favorite food category or send me your location!",
"quickReply": {
"items": [
{
"type": "action",
"imageUrl": "https://example.com/sushi.png",
"action": {
"type": "message",
"label": "Sushi",
"text": "Sushi"
}
},
{
"type": "action",
"imageUrl": "https://example.com/tempura.png",
"action": {
"type": "message",
"label": "Tempura",
"text": "Tempura"
}
},
{
"type": "action",
"action": {
"type": "location",
"label": "Send location"
}
}
]
}
}

我是這麼做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from linebot import LineBotApi
from linebot.models import (
TextSendMessage, QuickReply, QuickReplyButton,
MessageAction, LocationAction
)

access_token = os.getenv("LINE_CHANNEL_ACCESS_TOKEN", None)
line_bot_api = LineBotApi(access_token)

user_id = event.source.user_id

message = TextSendMessage(
text='Select your favorite food category or send me your location!',
quick_reply=QuickReply(
items=[
QuickReplyButton(
image_url='https://example.com/sushi.png',
action=MessageAction(label="Sushi", text="Sushi")
),
QuickReplyButton(
image_url='https://example.com/tempura.png',
action=MessageAction(label="Tempura", text="Tempura")
),
QuickReplyButton(
action=LocationAction(label="Send location")
),
]
)
)

line_bot_api.push_message(user_id, message)

還有很多 Actions 可以用呢!其他 Actions 的寫法以此類推~

跟大家分享個小技巧:
如果不確定某個部份該怎麼用,像是 LocationAction,可以直接去看 source code 哦!

Flex Message

官方文檔:Sending Flex Messages

可以先用 Flex Message Simulator 做出自己滿意的樣子,再把 JSON 貼到 Python code 裡面去。
問題來了,該怎麼弄呢?我來教你!

  1. 先用一個 string 存 JSON

    1
    2
    3
    flex_message_json_string = """
    <Flex Message Simulator 做好的 JSON 貼到這邊>
    """
  2. 轉成 dict 後,包成 FlexSendMessage 就行囉(需要但不是重點的我把它註解掉了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import json

    # from linebot import LineBotApi
    # from linebot.models import FlexSendMessage

    # access_token = os.getenv("LINE_CHANNEL_ACCESS_TOKEN", None)
    # line_bot_api = LineBotApi(access_token)

    # user_id = event.source.user_id

    flex_message_json_dict = json.loads(flex_message_json_string)
    message = FlexSendMessage(
    alt_text="xxx",
    contents=flex_message_json_dict
    )

    # 不推 push_message,因為有額度限制
    line_bot_api.push_message(user_id, message)

想看看做出來的效果如何?
Flex Message Simulator 右上角有個 Send... button,可以用那個測試哦!

官方文檔:Carousel template

可以直接參考「LINE Messaging API SDK for Python」來做。
不知道某個部份怎麼用?以 CarouselColumn 為例,一樣去翻 source code 吧!
再隨便舉個例子,若是 CameraAction 呢?一樣直接去洽詢 source code

Rich Menu

官方文檔:Rich menu

有兩種方式做這部分:

  1. 直接用官方後台做簡單的圖文選單
    LINE Official Account Manager → 選帳號 → 左側欄滑到底 → 聊天室相關 → 圖文選單)

  2. 用 code 來做圖文選單

我等等簡單介紹一下第二種方式,先給大家兩個不錯的參考資源:

前置作業

1
2
3
4
5
6
7
8
9
from linebot import LineBotApi
from linebot.models import (
RichMenu, RichMenuSize, RichMenuArea, RichMenuBounds,
URIAction, PostbackAction
)

channel_access_token = '<your channel access token>'

line_bot_api = LineBotApi(channel_access_token)

描述圖文選單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rich_menu_to_create = RichMenu(
size=RichMenuSize(width=2500, height=843),
selected=True,
name="Menu",
chat_bar_text="Menu",
areas=[
RichMenuArea(
bounds=RichMenuBounds(x=0, y=0, width=833, height=843),
action=PostbackAction(label='Record Taker', data='menu0', text='Record Taker')),
RichMenuArea(
bounds=RichMenuBounds(x=833, y=0, width=833, height=843),
action=PostbackAction(label='Cheer-up Buddy', data='menu1', text='Cheer-up Buddy')),
RichMenuArea(
bounds=RichMenuBounds(x=1666, y=0, width=833, height=843),
action=PostbackAction(label='Health Assistant', data='menu2', text='Health Assistant')),
]
)

如果有地方不確定怎麼用該怎麼辦?
應該已經聽到很厭煩了吧,沒錯,以 RichMenuArea 為例,去找 source code

創建圖文選單

1
2
rich_menu_id = line_bot_api.create_rich_menu(rich_menu=rich_menu_to_create)
print(rich_menu_id)

為圖文選單設圖

圖片的長寬大小要照前面的設定去製作哦!
以前面的描述為例,圖片尺寸必須要是 2500 × 843,像是:

(實用技巧:如果不擅長美工,可以用官方後台做完以後再下載下來,像我一樣xD)

1
2
with open('richmenu.png', 'rb') as f:
line_bot_api.set_rich_menu_image(rich_menu_id, 'image/png', f)

綁定圖文選單

1
line_bot_api.set_default_rich_menu(rich_menu_id)

其他操作詳見 line-bot-sdk-python

LIFF

官方文檔:LINE Front-end Framework

把它想成是一般的網頁,只是是開在 LINE 裡面。
(注意:Messaging API channel 不能添加 LIFF,不過因為我是創 Blockchain Service 的 channel,所以不受影響。想了解更多關於 LINE Blockchain?我後面會介紹到,期待一下~)
(上面的問題,如果仍舊想創 Messaging API channel,可以再創個 Login channel 解決)

製作教學

  1. 先寫好網頁的部分(HTML、CSS、JavaScript)

  2. HTML 檔的 <body> </body> 裡面加上

    1
    <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
  3. HTML 檔的 <script> </script> 或 JS 檔裡面加上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function initializeLiff(myLiffId) {
    liff.init({
    liffId: myLiffId
    })
    .then(() => {
    // start to use LIFF's api
    initializeApp();
    })
    .catch((err) => {
    });
    }

    initializeLiff('<LIFF ID>');

    以初始化

  4. 發送訊息到 LINE 聊天室&關閉頁面(其他功能請參見 LIFF v2 API reference

    1
    2
    3
    4
    5
    liff.sendMessages([{
    'type': 'text',
    'text': '要傳到聊天室的文字'
    }])
    liff.closeWindow();

生成 Endpoint URL

這個 URL 要提供給 LIFF。

我目前有試過兩種方法:

  1. 用 GitHub 的 gh-pages 分支部署,可參考這一篇,建議從「建立其他專案Repository」開始讀。完成後,就會拿到 https://<account>.github.io/<repo>/<folder> 這樣的 URL, index.htmlmain.js 這些靜態檔案要放在 <repo>/<folder>/ 底下哦!
  2. 用 ngrok,完成後會拿到 https://xxxxxxxxxxxx.ngrok.io 這樣的 URL
1
2
3
4
5
6
7
8
npm install --global http-server  # 裝 http-server
brew install --cask ngrok # 裝 ngrok

# 開始前,先 cd 到 index.html 的目錄下
http-server # 開啟 http-server

# 開新的終端機輸入
ngrok http 8080 # 讓 ngrok 幫我們做 forwarding

設定&測試

把該填的都填進 LIFF application 裡面吧。
LIFF URL 丟進聊天室,點開來,很酷的事情就會發生了xD

LINE Blockchain

可以參考我「當 Python 遇上了 LINE Blockchain」這一篇文。
我將會帶大家介紹 LINE Blockchain 的啟用以及設定,並用 Python 撰寫相關的 code。

Firebase

安裝

1
pipenv install firebase-admin

產生私密金鑰

專案總覽旁邊的⚙️ → 專案設定 → 服務帳戶 → 產生新的私密金鑰

讀寫資料庫

可以用 Firebase 的 Cloud Firestore,詳見官方文檔

初始化

1
2
3
4
5
6
7
8
9
10
11
12
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore

# 引用私密金鑰
cred = credentials.Certificate("path/to/serviceAccountKey.json")

# 初始化 firebase,注意不能重複初始化
firebase_admin.initialize_app(credential=cred)

# 初始化 firestore
db = firestore.client()

寫入資料庫(user_id 不用理它)

1
2
3
4
5
def write_to_db(user_id, key, value):
user_doc_ref = db.collection('users').document(user_id)
user_doc = user_doc_ref.get().to_dict()
user_doc[key] = value
user_doc_ref.set(user_doc)

讀取資料庫(user_id 不用理它)

1
2
3
4
5
6
7
8
def read_from_db(user_id, key):
try:
user_doc_ref = db.collection('users').document(user_id)
user_doc = user_doc_ref.get().to_dict()
value = user_doc[key]
except:
return ""
return value

上面那些 code 僅供參考,應該會有更好的寫法!

儲存圖片

可以用 Firebase 的 Storage,詳見官方文檔

初始化

1
2
3
4
5
6
7
8
9
10
11
12
import firebase_admin
from firebase_admin import credentials
from firebase_admin import storage

# 引用私密金鑰
cred = credentials.Certificate("path/to/serviceAccountKey.json")

# 初始化 firebase,注意不能重複初始化
firebase_admin.initialize_app(credential=cred, options={"storageBucket": "<project>.appspot.com"})

# 初始化 storage
bucket = storage.bucket()

從 LINE 上傳圖片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def upload_skin_image(user_id, message_id, file_name):

# 下載使用者傳的圖片到 server 上
message_content = line_bot_api.get_message_content(message_id)
temp_file_path = user_id
with open(temp_file_path, 'wb') as fd:
for chunk in message_content.iter_content():
fd.write(chunk)

# 設定 blob 資訊
saving_path = f'{user_id}/{file_name}'
blob = bucket.blob(saving_path)

# 上傳到 Firebase Storage
with open(temp_file_path, 'rb') as photo:
blob.upload_from_file(photo)

# 從 server 上移除
os.remove(temp_file_path)

# 檔案名稱寫到 db 上,方便之後取用
user_doc_ref = db.collection('users').document(user_id)
user_doc = user_doc_ref.get().to_dict()
user_doc['skin_image'] = saving_path
user_doc_ref.set(user_doc)

拿圖片的 URL

1
2
3
4
5
6
7
def get_skin_image_url(user_id):
user_doc_ref = db.collection('users').document(user_id)
user_doc = user_doc_ref.get().to_dict()
image = user_doc['skin_image']
blob = bucket.blob(image)
img_url = blob.generate_signed_url(datetime.timedelta(seconds=300), method='GET')
return img_url

一樣,上面那些 code 僅供參考,應該會有更好的寫法!

部署 ML Models

可以參考我「用 Algorithmia 部署 ML Models」這一篇文。
我將會帶大家用 Algorithmia 這個平台,把自己寫好的 ML model 包成 API,供其他 client 使用。

其他

LINE Bot 專屬 URL

可以用點連結的方式進頻道,詳見 Sharing a LINE Official Account

主動傳訊息

不需要 reply token,就能夠主動推訊息給使用者,詳見 Send push message
(注意:有使用上限額度,除非升級帳戶!我黑客松時,因為沒注意到這一點,讓我得重新註冊一個新的 channel,搞得我有點煩躁xD)


希望讀完這篇文章的您能夠有所收穫,我們下篇文見啦 😃