巴哈姆特 Chatbot 之亂:用 Ruby on Rails 接收 Webhook
六月底的時候發現巴哈姆特似乎想為他們推出的 Messaging APP (哈哈姆特)舉辦一個聊天機器人的比賽,看到之後想說還算蠻有趣的,所以我就跟朋友很隨意的組成一個團隊來開發。
跟大多數我們熟悉串接 Chatbot 的機制是類似的,我們可以用 Webhook 的方式接收一個來自使用者發送的訊息,然後再透過程式處理後回傳訊息給使用者。
了解 Webhook 機制
在程式設計中,我們常常會使用一種叫做「Hook(鉤子)」或者「Callback(回呼)」的機制,用比較好懂的角度去說明,他是一個在「程式執行中插入額外動作」
舉例來說,我們會有像這樣的程式
- 接收訊息
- 顯示訊息
假設我們要增加一個 Hook 就會變成像這樣
- 接收訊息
- Hooks (可能有多個)
- 顯示訊息
而 Webhook 就是指這個 Hook 利用 Web(網站)的方式執行,所以當這些 Messaging APP 收到訊息後,會利用 Webhook 做一些事情(像是發送給我們自己的伺服器)然後再繼續動作。
了解 Signature 機制
不過當我們收到一段訊息的時候,要怎麼知道這段訊息是來是正確的使用者?
這就要靠 Signature 機制來幫助我們,透過一個共用的密鑰(Token)來對訊息內容加密,當我們收到訊息的時候只要用同樣的密鑰對訊息加密,就會獲得一段驗證碼,當我們比對驗證碼跟發送者提供給我們相同時,就可以假設這是可信的訊息。
有些網站提供檔案下載時會提供 MD5 校驗碼也是同樣的原理。
以哈哈姆特的 Webhook 為例子,我們會從哈哈姆特收到一個 Webhook 請求,這個請求會包含類似下面的資訊。
- X-BAHA-DATA-SIGNATURE 標頭(Header)
- 內容(Ex. 某段訊息)
巴哈使用的是 SHA1 演算法(MD5 是另外一種),所以我們就要把內容用 SHA1 計算,再比對巴哈給我們的 X-BAHA-DATA-SIGNATURE
來驗證是否是來自巴哈,因為加密的密碼理論上只會有我們自己跟巴哈知道。
接收請求
如果你還沒有用過 Ruby on Rails 的話,可以參考龍哥所寫的為你自己學 Ruby on Rails 這本書,在網站上看到的部分就足夠你入門。
首先,我們希望有一個網址(Endpoint)可以接收請求,所以要在 config/routes.rb
定義一個控制器(Controller)來處理。
|
|
透過 Ruby 的 DSL 特性,我們就可以定義出一個叫做 /bahamut
的位址,用來接收巴哈姆特的 Webhook。然後在上面定義要使用 Webhook 控制器上面的 #bahamut
方法來處理這個位址的動作。
|
|
在這邊我們可能需要下一點指令才能測試,或者你可以使用 Postman 這套軟體來模擬 POST 請求。
POST 請求一般是我們送出表單的操作,所以無法直接用打開網頁的方式開啟。
|
|
然後我們就能看到我們的終端機(Terminal)出現了 Hello World
字樣。
如果我們希望巴哈能發送訊息到我們自己的本機電腦(localhost)就必須讓我們的電腦能在網路上被找到,這可以利用 Ngrok 這套軟體達成,透過 Ngrok 我們可以得到一個暫時性的網址,如此一來就能將本機測試的網站被巴哈呼叫到。
我想大家可能有疑問,就是是不是一定要用 Ruby on Rails 才能做到,實際上因為 Ruby on Rails 對初學者來說是最容易搭建出網站的選項,才會選擇使用。不然只要是任何能處理網的程式語言,都是可以直接用來寫 Chatbot 的,只不過像是 Ruby on Rails 這類網站開發框架,能幫我們省下學習這些基礎知識的時間。
驗證 Signature
因為處理簽章(Signature)的機制比較複雜,在物件導向類型的語言中,我們會設計一個 Class 來專門處理這件事情。
所以我們來製作一個服務物件(Service Object)叫做 Signature Verifer (簽章驗證器)來專們針對巴哈姆特傳入的簽章做驗證。
|
|
第一個步驟我們要設計驗證器的「初始化(Initialize)」階段要做什麼,我們預期會收到一個 HTTP 請求(request
)然後將裡面的簽章(x-baha-data-signature
)取出來,以及內容(對話訊息)取出來,這是我們在前面提到驗證是否是由巴哈發出的訊息所需要的資訊。
|
|
這個步驟是根據巴哈的文件將剛剛抓到的訊息跟聊天機器人的 Secret(秘鑰)做 SHA1 運算產生出我們自己計算的簽章,如此一來跟巴哈提供的比對,就會知道內容是不是一樣沒有被人偷偷竄改。
要特別注意的是
ENV['BAHA_SECRET']
這邊我是使用「環境變數」來儲存密鑰,這樣只有安裝伺服器的人會知道,就可以避面將這類敏感資訊放到程式碼之中。 在 Rails 5 之後,我們可以用
rails credentials:edit` 這個指令編輯一個加密的檔案,並把密鑰放到裡面,使用方法可以參考 Ruby on Rails 文件
|
|
最後再提供一個 valid?
方法,用來讓我們查詢是否正確就可以了!
我們修改一下 WebhookController
來做一個簡單的檢查。
|
|
假設我們透過 SignatureVerifer
驗證失敗的話,就回傳一個 JSON 資訊表示未驗證,並且設定 HTTP 的狀態碼為 401(未授權)的狀態。
JSON 是一種資料格式,常常用在不同伺服器溝同時當作交歡資料的格式,我們從巴哈收到的訊息也是 JSON 格式。
發送回應
既然我們已經可以接收訊息,如果使用者都沒有辦法收到任何回應的話肯定會覺得奇怪,所以下一步就是要能發送訊息給使用者。
哈哈姆特目前支援文字、圖片、貼圖跟事件幾種類型,其中事件是最容易做的,打好基底後也會變得更容易修改成支援其他類型的發送程式。
我們先來看一下從巴哈接收到的訊息會是怎樣的格式(JSON)
|
|
我們需要關注的只有 messaging
區塊的部分,裡面描述了「多個訊息」而每個訊息都會有「發送者」和「內容」兩個資訊。在上面這從官方文件複製的訊息範例中,使用者發送的內容是一段「文字(text)」
在 Rails 接收到之後,會自動的做好 JSON 解析的處理,所以我們可以直接像這樣使用。
|
|
在開始處理之前,我們需要先能夠發送訊息到哈哈姆特。因為步驟也是比較多的,所以我們需要製作一個 Sender (發送器)物件來處理。
|
|
首先我們在初始化階段要把「接收者」跟想要發送出去的「訊息」記錄起來。
|
|
接下來我們將巴哈文件上所提供的位置,以及一些發送請求需要的一些資訊製作出來。
像是
URI
這類轉換是用於 Ruby 處理發送 HTTP 請求所需要的,所以我們都先做好處理方便使用。而ENV['BAHA_TOKEN']
跟前面的ENV['BAHA_SECRET']
用途是一樣的,都是需要避免直接寫在程式內的數值。
|
|
因為我們要將訊息發給巴哈,巴哈再將訊息發給指定的使用者。
如果是 LINE 或者 Facebook Messenger 我們想對同一個人發訊息,在不同的 Chatbot 有不同的編號(ID)這樣就可以保護使用者不會被沒有授權的 Chatbot 騷擾,所以不論是發送還是接收,都需要透過巴哈的伺服器。
|
|
最後我們只需要將請求的內容(文字訊息)定義好,然後讓他可以發送出去,我們就能對使用者發送回應。
這邊的
recipient
通常會是我們收到的sender_id
我們可以在 Rails Console 裡面像這樣簡單測試是否可以發送訊息
|
|
自動回應相同訊息
我們將前面的程式整合後,可以改寫 WebhookController
讓他可以自動回應跟使用者相同的訊息。
|
|
我們透過將每一條訊息(messaging
)取出來,然後將接收者設定為發送者(sender_id
)並把訊息內容前面加上 PONG:
用來區別,確保確實是經過我們的 Chatbot 處理後才回應的。
PING/PONG 跟 Hello World 都有點像是一個習慣,通常我們用來測試一個伺服器是否有正常運作,就會透過發送 PING 然後確認伺服器有回應 PONG 來當判斷,如果想換成任何想要的訊息都是沒問題的。
小結
其實這些步驟在大多數情況應該被製作成一個 Gem (Ruby 的套件,可以想像成 Mod 之類的東西)直接使用,不過最近比較忙就沒有時間好好設計並且封裝成 Gem。
不過這篇文章的概念在處理各種類型的 Chatbot 是很好用的,如果有興趣的話也蠻推薦大家詳細了解一下。
留言