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_id`、`save_to_received`、`save_to_reply`、`echo2` 這四個函數,並且需要兩個資料模型,分別儲存`收到的對話`以及`回應的對話`。 # 建立資料模型 建立 `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 的文件](https://developers.line.me/en/docs/messaging-api/reference/#common-properties),我們知道要從 `params['events'][0]['source']` 底下去找 `groupId`、`roomId` 或者是 `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> ``` # 實測 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg85FHEL7CYrIQrode333JWYgEkIqrljuU_bdxvPZ4FyM_PXx5ud5lD1xy-qmDB63OolLgdwSTNcYoIyulCYuil9xK2Wp123dCUWMYQ8-phyWxxmNvpWmlMQV-cCJYIT9Y8HmwAKEcttC0/s1600/1.jpg) 成功! 失敗的在底下留言,謝謝。 # 本日重點 - 學會了判斷目前的頻道 - 學會了如何根據前後文作出不同的回應 # 接下來 剩沒幾天了,還有很多可以學的,我想知道你們比較想學些什麼? 接下來我們還可以做的事情有這些: - 現在的程式有點亂了,而且還有些問題,需要整理 - 讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應 - 讓卡米狗能抽籤 - 讓卡米狗能擷取用戶的使用者名稱以及大頭貼 - 讓卡米狗能接收及傳送貼圖 - 讓卡米狗能接收及傳送圖片 - 讓卡米狗能傳送含有按鈕的選單 - 讓卡米狗能查天氣 - 打造一個管理後台 - 讓卡米狗能發公告 - 製作小遊戲,比方說井字遊戲 或者你有想到,但上面沒列出來的也可以。 請在本文留言讓我知道你想學些什麼,可複選。

沒有留言: