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) 今天就講到這,明天講怎麼教卡米狗說話。

沒有留言: