iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 26
2

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

使用情境

我們希望的是這樣:

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>

實測

成功!

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

本日重點

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

接下來

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

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

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

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

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


上一篇
第二十五天:卡米狗學說話
下一篇
第二十七天:卡米狗見人說人話,見鬼說鬼話
系列文
只要有心,人人都可以做卡米狗33
0
luke90329
iT邦新手 5 級 ‧ 2018-01-14 10:10:49

怎麼辦都好想學QQ 不過就選幾個好了
讓卡米狗能查天氣
打造一個管理後台
讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應

好哦~好哦~

0
jerryw47
iT邦新手 5 級 ‧ 2018-01-14 17:08:59

希望可以學到這些(會不會有點太貪心? ? )
讓卡米狗能發公告
讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應
讓卡米狗能查天氣
打造一個管理後台

也很感謝你寫了這個序列文,讓我學到很多

好哦~好哦~

0
jerryw47
iT邦新手 5 級 ‧ 2018-01-14 19:09:27

想請問一下,我程式碼有輸入錯誤嗎?
因為不會輸入兩次就回話(沒有出現推齊功能)
https://ithelp.ithome.com.tw/upload/images/20180114/20107956tZRmkY5Ek0.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956c3aFupjIKS.png
https://ithelp.ithome.com.tw/upload/images/20180114/201079562eC6lHQHEH.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956hDVACQNnmo.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956FkPwyvGNoW.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956fwfakMLBSS.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956Ul58jG8LVo.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956s9j4GTUUHY.png
https://ithelp.ithome.com.tw/upload/images/20180114/20107956z1VWPzwM6W.png

看更多先前的回應...收起先前的回應...

需要觀察一下 heroku logs -t 在收到訊息時的 log

這是我輸入二次的“小凹”的結果
https://ithelp.ithome.com.tw/upload/images/20180114/20107956NGChXQ1sfD.png

recent_received_texts = Received.where(channel_id: channel_id).last(5)&.pluck(:text) 是一行程式,你似乎再中間多了一個換行。

我好像忘記要解釋這行程式碼QQ

有了,感謝你

0
ray19990613
iT邦新手 5 級 ‧ 2018-01-15 16:12:52

從以前就很想自己做一隻卡米狗來玩了,
現在終於成功了,
謝謝卡卡米 :)
只是...
能不能請你教我做類似行事曆的功能呢?
就是可以讓使用者設定事項,
時間快到的時候,
機器人會發訊息提醒使用者。
再次謝謝卡卡米的教學 :)))

我嘗試往這個方向講一點東西好了 但是不一定有時間講完就是了

0
nienst
iT邦新手 5 級 ‧ 2018-01-26 21:41:57

卡卡米你好
我想要學 "已讀機器人" ,可以知道有誰已讀
以及,匯出群組成員清單
以上主要是想用來 "清除幽靈人口"

LINE的群組有購買防翻機器人,但他的已讀機常常不回應
而且只列出已讀,功用不大。 因為我想踢的是 "未讀的幽靈人口"

目前只能一個一個去比對,找出三天以上沒已讀的名單

如果可以,我也想知道防翻機器人怎麼寫,呵呵

nienst iT邦新手 5 級‧ 2018-02-04 01:33:04 檢舉

照著API文件的說明,嘗試了很久,終於抓到 使用者名稱了
但是.... 竟然必須加好友才能得知名稱....(應該是想保護個資)
那這樣就無法寫出,匯出群組名單的功能了....

client = Line::Bot::Client.new { |config|
config.channel_secret = ""
config.channel_token = ""
}
response = client.get_profile("")
case response
when Net::HTTPSuccess then
contact = JSON.parse(response.body)
p contact['displayName']
p contact['pictureUrl']
p contact['statusMessage']
else
p "#{response.code} #{response.body}"
end

已讀機器人目前是無法用 line message api 作出來的。

因為 line 不會在有人已讀的時候通知你的聊天機器人,他只會在有人發訊息時通知你。

你頂多作最後發文時間機器人

nienst iT邦新手 5 級‧ 2018-02-06 01:33:48 檢舉

恩恩,而且有人 入群 退群 也是沒有通知的
我嘗試過 用 @ TAG人 看的到也只是文字
所以目前想不可以匯出群組名單的方法

0
curry_sun
iT邦新手 5 級 ‧ 2018-03-04 12:45:06

請問你怎麼用測試環境的呢?
有時候要測試一個方法的邏輯問題時,每次都要commit上去好耗時XD
有比較快的測試方法嗎??

看更多先前的回應...收起先前的回應...

謝謝你/images/emoticon/emoticon07.gif

看了兩天,還是不知所以然/images/emoticon/emoticon02.gif

XD 這要寫的話又可以寫很久了

nienst iT邦新手 5 級‧ 2018-03-18 00:53:34 檢舉

如果只是要測試一個方法的邏輯問題
可以用 irb 做簡單的指令測試

0
Doli
iT邦新手 5 級 ‧ 2018-04-03 23:52:33

cmd輸入rails generate model received channel_id text要建立資料模型出現錯誤,請問要怎麼改編碼問題呢

https://ithelp.ithome.com.tw/upload/images/20180403/20109159rauP4iCRqQ.png

https://ithelp.ithome.com.tw/upload/images/20180403/20109159TC7bagfhdp.png

看更多先前的回應...收起先前的回應...

試試看把資料夾路徑上有中文的全改成英文

Doli iT邦新手 5 級‧ 2018-04-04 11:30:34 檢舉

只有發現C碟下有一個"使用者"的資料夾是中文的(?這個算嗎但它不能改名字
https://ithelp.ithome.com.tw/upload/images/20180404/20109159nSXBTaljnJ.png
到這邊除了"使用者"資料夾 沒有其他中文
https://ithelp.ithome.com.tw/upload/images/20180404/20109159WTe3DHPZqL.png

https://ithelp.ithome.com.tw/upload/images/20180404/20109159F9JgT08LpJ.png

https://ithelp.ithome.com.tw/upload/images/20180404/20109159SQ3qhfS5xI.png

nienst iT邦新手 5 級‧ 2018-04-05 16:10:42 檢舉

https://ithelp.ithome.com.tw/upload/images/20180405/20108471MSrttdGjRH.jpg

路徑上的資料夾名稱 都要用英文 比較好
你的檔案是放在D槽

Doli iT邦新手 5 級‧ 2018-04-08 13:38:47 檢舉

還是一樣
https://ithelp.ithome.com.tw/upload/images/20180408/201091595PVqiQiFRQ.png

nienst iT邦新手 5 級‧ 2018-04-08 16:23:00 檢舉

你沒有所有的資料夾 都改英文
只要有心,人人都可以做卡米狗 也要改英文

nienst說的是對的

Doli iT邦新手 5 級‧ 2018-04-23 22:36:39 檢舉

好的謝謝成功ㄌ~~~~

0
nienst
iT邦新手 5 級 ‧ 2018-04-05 16:09:57

好像都無法刪除留言 ~"~

0
NekoShounen
iT邦新手 5 級 ‧ 2018-04-17 23:31:05

請問一下為什麼它一開始有反應後來就不理我了?
https://ithelp.ithome.com.tw/upload/images/20180417/20109266bvRJDmqq0j.jpg
https://ithelp.ithome.com.tw/upload/images/20180417/20109266xz488glQHF.jpg

https://ithelp.ithome.com.tw/upload/images/20180417/20109266AfeHhSmKlW.jpg
我在圖中好像有看到錯誤 但我不懂它指的是錯在哪裡
謝謝!

看更多先前的回應...收起先前的回應...

你似乎沒有 Received 這個東西

不好意思 這是什麼意思 是說我在哪裡漏打received嗎?

是你的程式打了 Received 但是你沒有先定義 Received,可能是文章中的

建立資料模型

你漏做了?

不是 我今天又檢查一遍之後發現是我之前漏安裝了PostgreSQL
安裝過後問題還是一樣 然後發現我之前有個地方打錯了
目前大概是這樣:
https://ithelp.ithome.com.tw/upload/images/20180418/201092664snF5AR4xN.jpg
https://ithelp.ithome.com.tw/upload/images/20180418/20109266hoLwxEDaZn.jpg
請問我該從哪裡補救... 還有如何把打錯的那個資料庫刪除?

把資料夾名稱改成英文的試試看

0
yvesen
iT邦新手 5 級 ‧ 2018-06-20 17:39:03

請問為什麼每次上傳後都還要跑下列步驟才能使用、不是有更新過就好了嗎?
$ gem install bundler
$ bundle update

請問你在哪裡跑這些步驟呢?

yvesen iT邦新手 5 級‧ 2018-06-21 13:55:51 檢舉

上傳到heroku之後

我要留言

立即登入留言