2018/1/10

第二十二天:用 Line Messaging API 實作關鍵字回覆

今天我要讓你能抓到一點寫程式的感覺,所以我們會一直不斷地修改程式碼,這麼做可以讓你對程式碼的操作更熟悉。

先從最簡單的功能開始作,今天的目標是讓卡米狗能針對關鍵字回應訊息。

程式碼的重構

在加程式碼之前,我們先整理一下目前的程式

  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 - 變數、常數、流程控制、迴圈

現在的程式已經足夠完美了,但有更精簡的寫法。

  # 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

用起來的效果是這樣:

那個「讓我想想...」是我們在第三天:作一隻最簡單的 Line 聊天機器人從 Line 後台作的設定,如果你不喜歡可以去後台把它關掉,它在這裡:

今天就講到這,明天講怎麼教卡米狗說話。

沒有留言: