2018/1/10
第二十二天:用 Line Messaging API 實作關鍵字回覆
markdown
今天我要讓你能抓到一點寫程式的感覺,所以我們會一直不斷地修改程式碼,這麼做可以讓你對程式碼的操作更熟悉。
先從最簡單的功能開始作,今天的目標是讓卡米狗能針對關鍵字回應訊息。
# 程式碼的重構
在加程式碼之前,我們先整理一下目前的程式
```
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
```
目前程式碼是這樣,我覺得有點太長了,我們要讓他更好閱讀。首先要制定一個目標。
```
def webhook
# 核心程式
reply_message = reply(received_message)
# 回覆訊息
reply_to_line(message)
# 回應 200
head :ok
end
```
只保留最重要的,當卡米狗看到 `received_message` 時,要回應 `reply_message`,剩下的東西都放到別處。我們先假設卡米狗只會對純文字有反應,並且只會回應純文字。
# 移出 client
```
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
```
我們把這段程式搬出去,變成這樣:
```
# Line Bot API 物件初始化
def line
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
定義一個方法叫作 `line`,他會回傳一個 client。這裡可以省略區域變數 client 不寫。
```
# Line Bot API 物件初始化
def line
Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
這麼寫的話,每次呼叫 `line` 時,就都會去作一次 `Line::Bot::Client.new`。我們可以把它保存起來,第二次呼叫 `line` 的時候就把保存起來的部分拿出來用,這樣作可以增加效能。
```
# Line Bot API 物件初始化
def line
return @line unless @line.nil?
@line = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
如果 `@line` 有值的話,直接回傳 `@line`,沒有值的話才作 `Line::Bot::Client.new` 並保存到 `@line`。
這裡用到了 `@`,`@` 開頭的變數是實體變數,跟區域變數不同的是,實體變數的記憶比較持久,區域變數只要函數執行完就消失,但實體變數可以持續存活到第二次之後的函數執行,甚至我可以在A函數保存實體變數,在B函數去使用實體變數。
關於實體變數,詳細的教學請參考:[為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈](https://railsbook.tw/chapters/05-ruby-basic-1.html#variable-and-constant)
現在的程式已經足夠完美了,但有更精簡的寫法。
```
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
`||` 是或的意思,這是一個很特殊的寫法,跟原本的程式碼效果幾乎相同,我就不多作解釋。沒學會 `||=` 也沒關係,這就是工程師der浪漫。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '移出 client'
}
# 傳送訊息
response = line.reply_message(reply_token, message)
# 回應 200
head :ok
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
```
改到這邊可以上傳程式碼測試一下,如果你不測也沒關係,因為我們還要繼續改。
# 移出 reply token
我們作一個函數,讓他自己去抓 reply token,我們關心的是要發什麼話,不關心 reply_token,所以我希望我們的主程式能變成這樣。
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
```
所以我們要實作函數 `reply_to_line`,他是一個傳入 message 後,透過 line api 傳送訊息出去,並傳回 HTTP response 的函數。
```
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
response = line.reply_message(reply_token, message)
end
```
可以再精簡為:
```
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
```
因為一個函數的傳回值是最後一行的執行結果。這就是工程師der浪漫。
其實我們大部分時間在作的事情都是搬移程式,其實寫程式就是一種整理的藝術。
你可以想像成我們東西一開始全都放在客廳。東西越來越多之後,客廳就會開始變亂。要整理客廳的方法就是買幾個櫃子後把東西放進櫃子。函數就是我們的櫃子。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
```
# 移出 message
這是現在的主程式:
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
```
我不希望在主程式看到這些:
```
{
type: 'text',
text: 'ㄅㄌㄅㄌㄅㄌ'
}
```
因為我們只在乎 `'ㄅㄌㄅㄌㄅㄌ'` 的部分,所以剩餘的部分都要盡量外移。這就像你不會想把垃圾放在桌上一樣,找個垃圾桶放垃圾就對了。
設定目標:
```
def webhook
# 設定回覆訊息
reply_text = '移出 message'
# 傳送訊息
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
```
這就是我們所希望的樣子,因此我們要修改 `reply_to_line` 這個函數。
```
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
其實就是把一開始的程式整個全搬到 `reply_to_line`。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = '移出 message'
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
```
現在的主程式已經比一開始乾淨許多,這時我們再來加功能,應該就會比較容易看出我們在加什麼功能。我們要加的功能是關鍵字回覆。
# 關鍵字回覆
我們希望主程式可以變成這樣:
```
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
```
所以我們需要兩個函數,一個是 `received_text`:傳回對方說的話,另一個函數是 `keyword_reply`:傳入對方說的話,傳回卡米狗應該說的話。
這種思考邏輯是先決定程式的大架構,再來描述細節。這就像在畫圖的時候,你會先打個草稿,草稿看起來OK了再去畫細節,這樣可以確保你不會太專注於細節而失去了整體比例。
```
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
```
現在這樣就表示你說什麼,卡米狗就會跟著說什麼。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
...下略
end
```
這程式碼可以運作,你可以上傳程式碼玩玩看。
這兩個函數現在這樣都還算未完成,他們都有一些缺陷。
先講關鍵字回覆。這裡應該要作成看見A回答B,而不是看見A回答A,我們還缺一個學習紀錄表讓卡米狗查。
而 `received_text` 的問題是,如果 Line 傳來的通知並不是訊息通知,而是比方說有人加你好友,或邀請你進入群組,或傳送貼圖、圖片、聲音、檔案都可能會導致我們的程式直接掛掉,因為 `params['events'][0]['message']['text']` 的值是空值,而我們沒有說當是空值的時候應該怎麼作。
# 處理 received_text 的空值問題
根據 Line Messaging API 的文件,當有人傳訊息來時,我們才會收到 `message` 這個 hash,也就是說,下面這行不一定有值。
```
params['events'][0]['message']
```
我們應該在他有值的時候才去取 ['text']
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
nil
else
message['text']
end
end
```
這是我們第一次用到了 if ,當 `message.nil?` 是空值的時候,我們就會傳回 `nil`。不是空值的話,就傳回 `message['text']`。
加入一點工程師的浪漫就會變成這樣:
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
```
有的時候不用太堅持什麼浪漫,因為這純粹只是工程師的自爽行為。
# 關鍵字回覆的學習紀錄表
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
```
在這裡定義了學習紀錄表 `keyword_mapping` 之後就作查表。當查表查不到內容的時候,就會傳回 nil。
這表示當查不到內容時,卡米狗應該要不回應,這需要修改其他函數。
```
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
這裡加一行 `return nil if reply_text.nil?`,當傳入值為空時表示不回應,後面的程式碼就不用作了。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
http = Net::HTTP.new(uri.host, uri.port)
http_request = Net::HTTP::Get.new(uri)
http_response = http.request(http_request)
render plain: JSON.pretty_generate({
request_class: request.class,
response_class: response.class,
http_request_class: http_request.class,
http_response_class: http_response.class
})
end
def translate_to_korean(message)
"#{message}油~"
end
end
```
用起來的效果是這樣:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFvB2Z-FMMJHkST4KTa555Wus0qw2BxKG3gi_710LZiHKWiftzM6ABpty-50xdVSqE_yzcr9cTSsRV685sim-q3FYXN0N0bOJPDi3O7F_AI05GGJKKusSEGV2SS5Xs0Vh0BynxavFRNCg/s1600/1.jpg)
那個「讓我想想...」是我們在[第三天:作一隻最簡單的 Line 聊天機器人](https://ithelp.ithome.com.tw/articles/10192928)從 Line 後台作的設定,如果你不喜歡可以去後台把它關掉,它在這裡:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDVJWISLeewPF0mQvK0ocHoSmWUIhyphenhyphentsvrm_nSoK0v1F6aaV6NeoXn_S67y2lJ_fSCN78wwKRYC2xKXss15CtU_vgnD7pobZgbwMoN149oncnoq92YsNx9Q-76IIGr4U9jQf68wkvMGAs/s1600/2.jpg)
今天就講到這,明天講怎麼教卡米狗說話。
訂閱:
張貼留言 (Atom)
沒有留言:
張貼留言