2025年8月7日 星期四

Python 學習筆記 : LINE Bot 開發 (四) 在 Mapleboard 佈署 Echo Bot 聊天機器人

經過一周奮戰, 這幾天完成 Mapleboard 的 HTTP API 函式執行平台建置, 終於可以回到這波 Mapleboard 架站的目標 : 利用 Mapleboard 當作 LINE Bot 的 webhook 後台伺服器來開發 LINE Bot 應用. 本篇先來接續之前用 ngrok 模擬後台伺服器實作 Echo Bot 的測試, 改用 Mapleboard 的 HTTP API 函式執行平台來做. 

本系列之前的測試文章參考 :


先摘要整理一下 LINE Bot 的製作流程 :

一. 建立 LINE Bot 與平台設定
  1. 申請 LINE Developers 帳號 :
    前往 LINE Developers 以個人 LINE 帳號登入.
  2. 建立 Channel (頻道) :
    選擇「Messaging API」類型, 設定 Channel 名稱, 圖示, 說明等資訊.
  3. 取得存取 LINE messaging API 之權限 : 
    •  Channel secret (通道密鑰) : 
      此密鑰是當後端應用程式收到 Webhook 請求時用來驗證 Webhook 傳入之請求真實性, 當使用者傳訊息給 LINE Bot, LINE 的伺服器會將該事件透過 Webhook 傳送給我們的後端伺服器, 同時在 HTTP Header 中夾帶一個 X-Line-Signature 簽章參數. 後端程式需要使用 Channel Secret 對請求內容進行 HMAC-SHA256 簽章驗證以來確保這是 LINE 官方伺服器傳來的請求而不是偽造的.
    • Channel access token (通道存取權杖) : 
      此權杖用來呼叫 LINE Messaging API 來推播或回覆訊息等. 

  4. 設定 Webhook URL :
    在 LINE Developers Console 中輸入 Webhook 網址, 啟用 Webhook 功能 (建議關閉「自動回應訊息」與「歡迎訊息」)

二. 撰寫 Webhook 程式 (例如 Flask)
  1. 安裝必要套件 :
    pip install flask line-bot-sdk
  2. 撰寫 Flask Webhook 程式 :
    # line_webhook.py
    from flask import Flask, request, abort
    from linebot import LineBotApi, WebhookHandler
    from linebot.exceptions import InvalidSignatureError
    from linebot.models import MessageEvent, TextMessage, TextSendMessage

    app = Flask(__name__)

    line_bot_api = LineBotApi('你的 Channel access token')
    handler = WebhookHandler('你的 Channel secret')

    @app.route("/line/webhook", methods=['POST'])
    def callback():
        signature = request.headers['X-Line-Signature']
        body = request.get_data(as_text=True)
        try:
            handler.handle(body, signature)
        except InvalidSignatureError:
            abort(400)
        return 'OK'

    @handler.add(MessageEvent, message=TextMessage)
    def handle_message(event):
        reply = f"你說的是:{event.message.text}"
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply))

    if __name__ == "__main__":
        app.run(port=8080)
三. 部署與測試 : 
  1. 部署 Webhook 程式到網際網路 :
    (1). Replit (簡易測試用)
    (2). Google Cloud Function (第一年免費) 
    (3). Render 或 Railway 等免費/低價平台
    (3). 自架伺服器 (可使用 Let's Encrypt 憑證 + Nginx)
  2. 使用 HTTPS 串接 LINE 平台 :
    LINE 要求 Webhook URL 必須是 公開的 HTTPS
我在第一篇測試中已經用個人的 LINE 帳號申請了 LINE Developers 帳號, 建立提供者 (Provider) 與 messaging API 頻道 (Channels), 在此頻道上創建了一個名為 "小狐狸事務所" 的聊天機器人, 取得 Channel secret (通道密碼) 與 Assertion signing key, 完成了 LINE messaging API 的註冊. 

第二篇測試中, 我建立了一個 LINE 官方帳號, 用來綁定第一篇中所創建的聊天機器人與後端應用程式, 並取得此聊天機器人之 Line ID 利用它將自己加入為好友以利後續測試.

第三篇測試中, 我登入 Line 開發者頁面後, 在 Basic settings 頁籤中建立一個頻道密鑰 (Channel secret); 在 Messaging API 頁籤中建立頻道存取權杖 (Channel access token), 這樣就可以利用 Mapleboard 的 HTTP API 函式執行平台撰寫 Flask 應用程式作為 LINE Bot 的 webhook. 關於此函式執行平台參考 :


以下測試是根據 HTTP API 函式執行平台 (4) 這篇優化過的版本, LINE Bot 後端應用程式需依照下列格式撰寫 :

# func_module.py
def main(request, **kwargs):
    config=kwargs.get('config', {})  # 預設為空 dict
    result='do something'
    return result

所有金鑰或權杖等資訊都儲存在主程式目錄下的 .env 環境變數檔案裡, 主程式 serverless.py 會讀取此檔案並將內容放在 config 參數 (字典) 中以關鍵字參數 config 傳遞給被載入之函式模組的 main() 函式, 呼叫字典的 get() 方法即可取出此


1. 添加 LINE messagin API 環境變數 :

因為在 HTTP API 函式執行平台 (4) 這篇裡已重新規劃將所有權杖與金鑰集中於平台的環境變數檔 .env 中統一管理, 主程式 serverless.py 會讀取 .env 檔後將全部內容放在 config 字典中傳遞給被載入模組的 main() 函式, 因此要執行的函式都放在 Mapleboard 的 ~/flask_apps/serverless/functions 下, 所以我先在此資料夾下用 nano 編輯一個 .env 環境變數檔來儲存 LINE Bot 的密鑰與權杖 : 

tony1966@LX2438:~/flask_apps/serverless$ sudo nano .env  

在最後面添加 LINE_CHANNEL_SECRET 與 LINE_CHANNEL_ACCESS_TOKEN 這兩個變數 :




按 Ctrl+O 存檔後按 Ctrl+X 跳出 nano, 然後必需重啟服務, 這樣 serverless.py 才會讀取新的 .env 內容 :

tony1966@LX2438:~/flask_apps/serverless$ sudo systemctl restart serverless 


2. 安裝 line-bot-sdk 套件 :

在 Mapleboard 用 pip3 安裝 line-bot-sdk 套件 :

pip3 install line-bot-sdk 

tony1966@LX2438:/$ pip3 install line-bot-sdk   
Defaulting to user installation because normal site-packages is not writeable
Collecting line-bot-sdk
  Downloading line_bot_sdk-3.18.0-py2.py3-none-any.whl (824 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 824.9/824.9 KB 2.1 MB/s eta 0:00:00
Collecting future>=1.0.0
  Downloading future-1.0.0-py3-none-any.whl (491 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 491.3/491.3 KB 3.0 MB/s eta 0:00:00
Collecting urllib3<3,>=2.0.5
  Downloading urllib3-2.5.0-py3-none-any.whl (129 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 129.8/129.8 KB 5.1 MB/s eta 0:00:00
Collecting Deprecated>=1.2.18
  Downloading Deprecated-1.2.18-py2.py3-none-any.whl (10.0 kB)
Requirement already satisfied: pydantic<3,>=2.0.3 in /home/tony1966/.local/lib/python3.10/site-packages (from line-bot-sdk) (2.11.7)
Collecting aenum<4,>=3.1.11
  Downloading aenum-3.1.16-py3-none-any.whl (165 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 165.6/165.6 KB 6.5 MB/s eta 0:00:00
Collecting aiohttp<4,>=3.10.9
  Downloading aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.7 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.7/1.7 MB 7.0 MB/s eta 0:00:00
Requirement already satisfied: python_dateutil<3,>=2.5.3 in /home/tony1966/.local/lib/python3.10/site-packages (from line-bot-sdk) (2.9.0.post0)
Requirement already satisfied: requests<3,>=2.32.3 in /home/tony1966/.local/lib/python3.10/site-packages (from line-bot-sdk) (2.32.4)
Collecting frozenlist>=1.1.1
  Downloading frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (224 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 224.6/224.6 KB 4.7 MB/s eta 0:00:00
Collecting propcache>=0.2.0
  Downloading propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (201 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 201.9/201.9 KB 6.9 MB/s eta 0:00:00
Collecting aiosignal>=1.4.0
  Downloading aiosignal-1.4.0-py3-none-any.whl (7.5 kB)
Collecting yarl<2.0,>=1.17.0
  Downloading yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (323 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 323.8/323.8 KB 6.5 MB/s eta 0:00:00
Requirement already satisfied: attrs>=17.3.0 in /home/tony1966/.local/lib/python3.10/site-packages (from aiohttp<4,>=3.10.9->line-bot-sdk) (23.2.0)
Collecting aiohappyeyeballs>=2.5.0
  Downloading aiohappyeyeballs-2.6.1-py3-none-any.whl (15 kB)
Collecting async-timeout<6.0,>=4.0
  Downloading async_timeout-5.0.1-py3-none-any.whl (6.2 kB)
Collecting multidict<7.0,>=4.5
  Downloading multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (242 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 242.5/242.5 KB 5.3 MB/s eta 0:00:00
Requirement already satisfied: wrapt<2,>=1.10 in /usr/lib/python3/dist-packages (from Deprecated>=1.2.18->line-bot-sdk) (1.13.3)
Requirement already satisfied: typing-extensions>=4.12.2 in /home/tony1966/.local/lib/python3.10/site-packages (from pydantic<3,>=2.0.3->line-bot-sdk) (4.14.0)
Requirement already satisfied: typing-inspection>=0.4.0 in /home/tony1966/.local/lib/python3.10/site-packages (from pydantic<3,>=2.0.3->line-bot-sdk) (0.4.1)
Requirement already satisfied: pydantic-core==2.33.2 in /home/tony1966/.local/lib/python3.10/site-packages (from pydantic<3,>=2.0.3->line-bot-sdk) (2.33.2)
Requirement already satisfied: annotated-types>=0.6.0 in /home/tony1966/.local/lib/python3.10/site-packages (from pydantic<3,>=2.0.3->line-bot-sdk) (0.7.0)
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python_dateutil<3,>=2.5.3->line-bot-sdk) (1.16.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/tony1966/.local/lib/python3.10/site-packages (from requests<3,>=2.32.3->line-bot-sdk) (2024.2.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/lib/python3/dist-packages (from requests<3,>=2.32.3->line-bot-sdk) (3.3)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/tony1966/.local/lib/python3.10/site-packages (from requests<3,>=2.32.3->line-bot-sdk) (3.4.2)
Installing collected packages: aenum, urllib3, propcache, multidict, future, frozenlist, Deprecated, async-timeout, aiohappyeyeballs, yarl, aiosignal, aiohttp, line-bot-sdk
Successfully installed Deprecated-1.2.18 aenum-3.1.16 aiohappyeyeballs-2.6.1 aiohttp-3.12.15 aiosignal-1.4.0 async-timeout-5.0.1 frozenlist-1.7.0 future-1.0.0 line-bot-sdk-3.18.0 multidict-6.6.3 propcache-0.3.2 urllib3-2.5.0 yarl-1.20.1


3. 撰寫後端應用程式 :

為了搭配 serverless 函式執行平台架構, 撰寫處理 LINE webhook 請求與產生 echo 回覆的後端程式 linebot_echo.py 如下 : 

# linebot_echo.py 
from flask import abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage

def main(request, **kwargs):
    # 取得主程式讀取 .env 後以 config 字典傳遞之 LINE 權杖與金鑰
    config=kwargs.get('config', {})
    secret=config.get('LINE_CHANNEL_SECRET')
    token=config.get('LINE_CHANNEL_ACCESS_TOKEN')
    # 檢查是否有傳遞密鑰與權杖
    if not secret or not token:
        return {"error": "LINE_CHANNEL_SECRET or LINE_CHANNEL_ACCESS_TOKEN is missing"}
    # 建立呼叫 LINE API 與處理 webhook 的物件
    line_bot_api=LineBotApi(token)  # 發送回覆訊息的 API 客戶端
    handler=WebhookHandler(secret)  # 驗證簽章與分派事件的 webhook 處理器
    # 註冊 MessageEvent + TextMessage 事件的處理器
    # 注意:這段必須寫在 handler.handle() 之前, 否則事件不會被處理
    @handler.add(MessageEvent, message=TextMessage)
    def handle_message(event):
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=event.message.text)
            )
    # 取得 X-Line-Signature (用於簽章驗證) 與 request body (即使用者訊息)
    signature=request.headers.get("X-Line-Signature", "")
    body=request.get_data(as_text=True)
    # 驗證簽章是否正確, 防止偽造的請求 (確保是 LINE 平台發出的請求)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400, "Invalid signature")
    # 處理成功回傳狀態訊息 (LINE 要求回應 HTTP 200 才視為成功)
    return {"status": "ok"}

此程式需注意的地方是註冊 LINE 事件處理器必須放在呼叫 handler.handle() 之前; 其次是末了處理成功時要傳回一個 ok 訊息, Flask 預設會將 {"status": "ok"} 轉換成 JSON 格式的回應, 這會回傳 HTTP 狀態碼 200 OK, 以符合 LINE 訊息伺服器要求. 

我用線上管理功能新增此 linebot_echo.py 模組 : 




將滑鼠移到 linebot_echo 後面的 "執行" 連結即可複製此函式模組的請求網址 (這是需要驗證的 POST 請求 webhook, 無法用 GET 直接執行) : 

# https://flask.tony1966.cc/function/linebot_echo 

接下來到 Line 開發者控制台設定 Webhook URL :


切換到第二個頁籤 "Messaging API" 後往下拉到 "Webhook setting" 欄按 "Edit" 鈕, 




在 Webhook URL 框中輸入後端程式網址 https://flask.tony1966.cc/function/linebot_echo, 按 Update 鈕更新網址 :




按 "Verify" 鈕驗證 Webhook 網址 :





出現 Success OK 表示 Webhook 可順利連線後台 Flask 伺服器 : 




這樣就完成後台程式的設定了, 開啟手機 LINE App, 點選 "好友名單" 或 "群組", 切換到 "官方帳號", 找到前面測試中建立 "小狐狸事務所聊天機器人", 點進去輸入聊天訊息, 果然收到訊息被原封不動傳回來 : 




收工啦! 

沒有留言 :