2018/1/14

第二十六天:卡米狗推齊

今天要作的是卡米狗的推齊功能,也就是當看到有兩次以上有人說出相同的句子,那麼就跟著說的功能。要作到這件事,卡米狗必須要有一點記性才行,所以我們必須記錄每個群組中所發生的對話。當有人說出一句話時,就檢查最近有沒有人也說出相同的話,如果有的話卡米狗就跟著說。

使用情境

我們希望的是這樣:

B哥:「采瑤生日快樂~~」

小昕:「采瑤生日快樂~~」

卡米狗:「采瑤生日快樂~~」

但事實上是這樣:

B哥:「采瑤生日快樂~~」

小昕:「采瑤生日快樂~~」

卡米狗:「采瑤生日快樂~~」

毛毛:「采瑤生日快樂~~」

卡米狗:「采瑤生日快樂~~」

卡米狗不應該推齊兩次的,因為正常人推齊只會推一次,所以卡米狗要記得自己上次說了什麼。

推齊的邏輯

整理了一下之後,我們可以寫一個大概的程式碼如下:

def 推齊(channel_id, received_text)
  如果在 channel_id 最近沒人講過 received_text,卡米狗就不回應
  如果在 channel_id 卡米狗上一句回應是 received_text,卡米狗就不回應
  回應 received_text
end

這種不能執行的程式碼稱為虛擬碼,是用來表達邏輯、幫助思考和討論用的。

channel_id 代表目前的群組、聊天室或私聊的 ID,我們這裡姑且通稱為頻道 ID。

修改主程式

這是目前的程式碼:

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 

在卡米狗中,推齊功能的順位是最低的,所以我們會把推齊擺在關鍵字回覆的後面。

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 

我們還需要記錄對話:

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 

主程式大概就是這樣了。

我們需要實作 channel_idsave_to_receivedsave_to_replyecho2 這四個函數,並且需要兩個資料模型,分別儲存收到的對話以及回應的對話

建立資料模型

建立 received 資料模型:rails generate model received channel_id text

D:\只要有心,人人都可以作卡米狗\ironman>rails generate model received channel_id text
      invoke  active_record
      create    db/migrate/20180113153959_create_receiveds.rb
      create    app/models/received.rb
      invoke    test_unit
      create      test/models/received_test.rb
      create      test/fixtures/receiveds.yml

D:\只要有心,人人都可以作卡米狗\ironman>

建立 reply 資料模型:rails generate model reply channel_id text

D:\只要有心,人人都可以作卡米狗\ironman>rails generate model reply channel_id text
      invoke  active_record
      create    db/migrate/20180113154217_create_replies.rb
      create    app/models/reply.rb
      invoke    test_unit
      create      test/models/reply_test.rb
      create      test/fixtures/replies.yml

D:\只要有心,人人都可以作卡米狗\ironman>

進行資料庫遷移:rails db:migrate

D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate
== 20180113153959 CreateReceiveds: migrating ==================================
-- create_table(:receiveds)
   -> 0.5013s
== 20180113153959 CreateReceiveds: migrated (0.5027s) =========================

== 20180113154217 CreateReplies: migrating ====================================
-- create_table(:replies)
   -> 0.0013s
== 20180113154217 CreateReplies: migrated (0.0024s) ===========================


D:\只要有心,人人都可以作卡米狗\ironman>

頻道 ID

根據 Line Messaging API 的文件,我們知道要從 params['events'][0]['source'] 底下去找 groupIdroomId 或者是 userId

如果對話是發生在群組,groupId 就會有值,如果對話是發生在聊天室,roomId 就會有值。

所以我們要這樣寫:

  # 頻道 ID
  def channel_id
    source = params['events'][0]['source']
    return source['groupId'] unless source['groupId'].nil?
    return source['roomId'] unless source['roomId'].nil?
    source['userId']
  end

可以浪漫一點:

  # 頻道 ID
  def channel_id
    source = params['events'][0]['source']
    source['groupId'] || source['roomId'] || source['userId']
  end

儲存對話

在儲存前應該先檢查有沒有值,因為 received_text 不一定有值。

  # 儲存對話
  def save_to_received(channel_id, received_text)
    return if received_text.nil?
    Received.create(channel_id: channel_id, text: received_text)
  end

儲存回應

  # 儲存回應
  def save_to_reply(channel_id, reply_text)
    return if reply_text.nil?
    Reply.create(channel_id: channel_id, text: reply_text)
  end

推齊

按照我們一開始講的虛擬碼邏輯去寫:

  def echo2(channel_id, received_text)
    # 如果在 channel_id 最近沒人講過 received_text,卡米狗就不回應
    recent_received_texts = Received.where(channel_id: channel_id).last(5)&.pluck(:text)
    return nil unless received_text.in? recent_received_texts

    # 如果在 channel_id 卡米狗上一句回應是 received_text,卡米狗就不回應
    last_reply_text = Reply.where(channel_id: channel_id).last&.text
    return nil if last_reply_text == received_text

    received_text
  end

發布

對一下程式碼

require 'line/bot'
class KamigoController < ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 

  # 頻道 ID
  def channel_id
    source = params['events'][0]['source']
    source['groupId'] || source['roomId'] || source['userId']
  end

  # 儲存對話
  def save_to_received(channel_id, received_text)
    return if received_text.nil?
    Received.create(channel_id: channel_id, text: received_text)
  end

  # 儲存回應
  def save_to_reply(channel_id, reply_text)
    return if reply_text.nil?
    Reply.create(channel_id: channel_id, text: reply_text)
  end

  def echo2(channel_id, received_text)
    # 如果在 channel_id 最近沒人講過 received_text,卡米狗就不回應
    recent_received_texts = Received.where(channel_id: channel_id).last(5)&.pluck(:text)
    return nil unless received_text.in? recent_received_texts

    # 如果在 channel_id 卡米狗上一句回應是 received_text,卡米狗就不回應
    last_reply_text = Reply.where(channel_id: channel_id).last&.text
    return nil if last_reply_text == received_text

    received_text
  end

  # 取得對方說的話
  def received_text
    message = params['events'][0]['message']
    message['text'] unless message.nil?
  end

  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == '卡米狗學說話;'

    received_text = received_text[7..-1]
    semicolon_index = received_text.index(';')

    # 找不到分號就跳出
    return nil if semicolon_index.nil?

    keyword = received_text[0..semicolon_index-1]
    message = received_text[semicolon_index+1..-1]

    KeywordMapping.create(keyword: keyword, message: message)
    '好哦~好哦~'
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    KeywordMapping.where(keyword: received_text).last&.message
  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

上傳程式碼囉~

Heroku 上的資料庫遷移

要在上傳程式碼之後才能作資料庫遷移,因為資料庫遷移需要讀取資料庫遷移檔。Heroku 上的資料庫遷移指令是 heroku run rake db:migrate

D:\只要有心,人人都可以作卡米狗\ironman>heroku run rake db:migrate
Running rake db:migrate on people-all-love-kamigo... up, run.4769 (Free)
D, [2018-01-13T16:57:33.099237 #4] DEBUG -- :    (0.6ms)  SELECT pg_try_advisory_lock(8162367372296191845)
D, [2018-01-13T16:57:33.115389 #4] DEBUG -- :    (2.9ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
I, [2018-01-13T16:57:33.116984 #4]  INFO -- : Migrating to CreateReceiveds (20180113153959)
D, [2018-01-13T16:57:33.119682 #4] DEBUG -- :    (0.6ms)  BEGIN
== 20180113153959 CreateReceiveds: migrating ==================================
-- create_table(:receiveds)
D, [2018-01-13T16:57:33.166042 #4] DEBUG -- :    (45.6ms)  CREATE TABLE "receiveds" ("id" bigserial primary key, "channel_id" character varying, "text" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
   -> 0.0463s
== 20180113153959 CreateReceiveds: migrated (0.0464s) =========================

D, [2018-01-13T16:57:33.170513 #4] DEBUG -- :   SQL (0.7ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20180113153959"]]
D, [2018-01-13T16:57:33.173887 #4] DEBUG -- :    (3.1ms)  COMMIT
I, [2018-01-13T16:57:33.174003 #4]  INFO -- : Migrating to CreateReplies (20180113154217)
D, [2018-01-13T16:57:33.174944 #4] DEBUG -- :    (0.6ms)  BEGIN
== 20180113154217 CreateReplies: migrating ====================================
-- create_table(:replies)
D, [2018-01-13T16:57:33.184287 #4] DEBUG -- :    (8.8ms)  CREATE TABLE "replies" ("id" bigserial primary key, "channel_id" character varying, "text" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
   -> 0.0093s
== 20180113154217 CreateReplies: migrated (0.0093s) ===========================

D, [2018-01-13T16:57:33.185682 #4] DEBUG -- :   SQL (0.6ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20180113154217"]]
D, [2018-01-13T16:57:33.187624 #4] DEBUG -- :    (1.7ms)  COMMIT
D, [2018-01-13T16:57:33.193606 #4] DEBUG -- :   ActiveRecord::InternalMetadata Load (2.0ms)  SELECT  "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2  [["key", "environment"], ["LIMIT", 1]]
D, [2018-01-13T16:57:33.201843 #4] DEBUG -- :    (0.5ms)  BEGIN
D, [2018-01-13T16:57:33.204353 #4] DEBUG -- :    (1.6ms)  COMMIT
D, [2018-01-13T16:57:33.205359 #4] DEBUG -- :    (0.7ms)  SELECT pg_advisory_unlock(8162367372296191845)

D:\只要有心,人人都可以作卡米狗\ironman>

實測

成功!

失敗的在底下留言,謝謝。

本日重點

  • 學會了判斷目前的頻道
  • 學會了如何根據前後文作出不同的回應

接下來

剩沒幾天了,還有很多可以學的,我想知道你們比較想學些什麼?

接下來我們還可以做的事情有這些:

  • 現在的程式有點亂了,而且還有些問題,需要整理
  • 讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應
  • 讓卡米狗能抽籤
  • 讓卡米狗能擷取用戶的使用者名稱以及大頭貼
  • 讓卡米狗能接收及傳送貼圖
  • 讓卡米狗能接收及傳送圖片
  • 讓卡米狗能傳送含有按鈕的選單
  • 讓卡米狗能查天氣
  • 打造一個管理後台
  • 讓卡米狗能發公告
  • 製作小遊戲,比方說井字遊戲

或者你有想到,但上面沒列出來的也可以。

請在本文留言讓我知道你想學些什麼,可複選。

沒有留言: