2018/1/15

第二十七天:卡米狗見人說人話,見鬼說鬼話

在[第二天:認識卡米狗](https://ithelp.ithome.com.tw/articles/10192575)提到過,見人說人話,見鬼說鬼話功能是考慮到多個群組都教了相同的關鍵字時,卡米狗應該在每個群組做出不同的回應,這樣才不會被討厭,於是就加入了這樣的功能。當有人說「姆咪姆咪」時,卡米狗會先檢查這個群組有沒有人教過看到「姆咪姆咪」要回應,如果教過多次,就回應最後一次學過的內容,如果都沒學過,那麼就再檢查其他群組有沒有學過「姆咪姆咪」。 也就是說,學說話指令在儲存時,應該也要儲存是在哪個頻道學會的。 # 修改學說話指令 目前的學說話指令: ``` # 學說話 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 learn(channel_id, received_text) ...略 KeywordMapping.create(channel_id: channel_id, keyword: keyword, message: message) ...略 end ``` 重點是多傳入一個參數 `channel_id`,然後存入 `KeywordMapping`。 # 修改關鍵字回覆 在關鍵字回覆的部分,原本是: ``` # 關鍵字回覆 def keyword_reply(received_text) KeywordMapping.where(keyword: received_text).last&.message end ``` 則是改為: ``` # 關鍵字回覆 def keyword_reply(channel_id, received_text) message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&.message return message unless message.nil? KeywordMapping.where(keyword: received_text).last&.message end ``` 多加了這兩行: ``` message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&.message return message unless message.nil? ``` 這兩行的意思是,先找同一個頻道內教過的關鍵字,如果有找到的話就直接回傳。 如果你想要深入學習資料模型的查詢,官方也有提供中文版的說明文件,在這裡:[Active Record 查詢](https://rails.ruby.tw/active_record_querying.html#%E6%A2%9D%E4%BB%B6)。 # 主程式 要記得把參數也傳給剛剛改好的函數,原本的主程式是這樣: ``` 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 ``` 要改成: ``` def webhook # 學說話 reply_text = learn(channel_id, received_text) # 關鍵字回覆 reply_text = keyword_reply(channel_id, received_text) if reply_text.nil? ...略 end ``` 這樣就作完了嗎!? 還沒呢,我們的 `KeywordMapping` 根本沒有 `channel_id` 欄位呀! # 在 KeywordMapping 資料模型中新增欄位 我們需要使用資料庫遷移的方式來對 KeywordMapping 新增欄位,首先要先建立一個資料庫遷移檔。 指令是 `rails generate migration` 加上註解: ``` rails generate migration add_channel_id_to_keyword_reply ``` ``` D:\只要有心,人人都可以作卡米狗\ironman>rails generate migration add_channel_id_to_keyword_reply invoke active_record create db/migrate/20180114163555_add_channel_id_to_keyword_reply.rb D:\只要有心,人人都可以作卡米狗\ironman> ``` 生成了一個檔案在 `db/migrate` 裡面,我們要在這個資料庫遷移檔裡打一點字,這是他目前的樣子: ``` class AddChannelIdToKeywordReply < ActiveRecord::Migration[5.1] def change end end ``` 要加一個欄位的話,要這樣寫: ``` class AddChannelIdToKeywordReply < ActiveRecord::Migration[5.1] def change add_column :keyword_mappings, :channel_id, :string end end ``` 在 `add_column` 後面第一個參數是`資料表名稱`,第二個參數是`要新增的欄位名稱`,以及第三個參數:`要新增的欄位格式`。欄位格式的話,沒意外通常都會是 `:string`。 這裡寫好之後存檔,就可以作資料庫遷移了。為什麼我知道是這樣寫呢?文件在這裡:[Active Record 遷移](https://rails.ruby.tw/active_record_migrations.html#%E6%96%B0%E5%BB%BA%E7%8D%A8%E7%AB%8B%E7%9A%84%E9%81%B7%E7%A7%BB) # 資料庫遷移 ``` D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate == 20180114163555 AddChannelIdToKeywordReply: migrating ======================= -- add_column(:keyword_mappings, :channel_id, :string) -> 0.0012s == 20180114163555 AddChannelIdToKeywordReply: migrated (0.0020s) ============== D:\只要有心,人人都可以作卡米狗\ironman> ``` 如果資料庫遷移檔沒打錯字的話,就會看到這個結果。 # 進行實測 首先上傳程式碼,要養成開著 `heroku logs -t` 的習慣。 測了一下會發現: ``` 2018-01-14T16:56:36.918562+00:00 app[web.1]: I, [2018-01-14T16:56:36.918392 #4] INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Started POST "/kamigo/webhook" for 203.104.146.154 at 2018-01-14 16:56:36 +0000 2018-01-14T16:56:36.920295+00:00 app[web.1]: I, [2018-01-14T16:56:36.920209 #4] INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Processing by KamigoController#webhook as */* 2018-01-14T16:56:36.920486+00:00 app[web.1]: I, [2018-01-14T16:56:36.920397 #4] INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Parameters: {"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}], "kamigo"=>{"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}]}} 2018-01-14T16:56:36.920998+00:00 app[web.1]: W, [2018-01-14T16:56:36.920917 #4] WARN -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Can't verify CSRF token authenticity. 2018-01-14T16:56:36.925356+00:00 app[web.1]: D, [2018-01-14T16:56:36.925257 #4] DEBUG -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] KeywordMapping Load (1.6ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3 [["channel_id", "Uc68d82df46b7899e7d716f396ae8e91a"], ["keyword", "A"], ["LIMIT", 1]] 2018-01-14T16:56:36.925763+00:00 app[web.1]: I, [2018-01-14T16:56:36.925658 #4] INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms) 2018-01-14T16:56:36.927283+00:00 app[web.1]: F, [2018-01-14T16:56:36.927195 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] 2018-01-14T16:56:36.927428+00:00 app[web.1]: F, [2018-01-14T16:56:36.927362 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column keyword_mappings.channel_id does not exist 2018-01-14T16:56:36.927431+00:00 app[web.1]: LINE 1: ...keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_m... 2018-01-14T16:56:36.927432+00:00 app[web.1]: ^ 2018-01-14T16:56:36.927438+00:00 app[web.1]: : SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3): 2018-01-14T16:56:36.927567+00:00 app[web.1]: F, [2018-01-14T16:56:36.927496 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] 2018-01-14T16:56:36.927701+00:00 app[web.1]: F, [2018-01-14T16:56:36.927580 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] app/controllers/kamigo_controller.rb:82:in `keyword_reply' 2018-01-14T16:56:36.927702+00:00 app[web.1]: [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] app/controllers/kamigo_controller.rb:10:in `webhook' 2018-01-14T16:56:36.929306+00:00 heroku[router]: at=info method=POST path="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd="203.104.146.154" dyno=web.1 connect=0ms service=11ms status=500 bytes=1827 protocol=https ``` 我先把前面那些多餘的字移除: ``` Started POST "/kamigo/webhook" for 203.104.146.154 at 2018-01-14 16:56:36 +0000 Processing by KamigoController#webhook as */* Parameters: {"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}], "kamigo"=>{"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}]}} Can't verify CSRF token authenticity. KeywordMapping Load (1.6ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3 [["channel_id", "Uc68d82df46b7899e7d716f396ae8e91a"], ["keyword", "A"], ["LIMIT", 1]] Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms) ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column keyword_mappings.channel_id does not exist LINE 1: ...keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_m... ^ SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3): app/controllers/kamigo_controller.rb:82:in `keyword_reply' app/controllers/kamigo_controller.rb:10:in `webhook' at=info method=POST path="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd="203.104.146.154" dyno=web.1 connect=0ms service=11ms status=500 bytes=1827 protocol=https ``` 我們要關注的重點在: ``` Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms) ``` 當你看到 `500 Internal Server Error`,表示程式跑到一半就掛了,掛點原因通常會寫在這個訊息後面。 一個正常的 Log 是長這樣: ``` Completed 200 OK in 269ms (ActiveRecord: 9.9ms) ``` 掛點原因: ``` ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column keyword_mappings.channel_id does not exist LINE 1: ...keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_m... ^ SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3): app/controllers/kamigo_controller.rb:82:in `keyword_reply' app/controllers/kamigo_controller.rb:10:in `webhook' ``` 他說:`ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column keyword_mappings.channel_id does not exist`,意思是 `keyword_mappings` 表格裡面沒有 `channel_id` 這個欄位。 這個叫做 `exception message`。一般來說遇到絕大多數的問題都可以拿 `exception message` 去餵給 google ,就能得到問題的答案。不過看到這裡應該就能猜到是忘記作 Heroku 上的資料庫遷移了。 另外,最後面的那兩行: ``` app/controllers/kamigo_controller.rb:82:in `keyword_reply' app/controllers/kamigo_controller.rb:10:in `webhook' ``` 這個叫做 `stack trace`。 意思是他死在 `kamigo_controller.rb` 的第 `82` 行,是在 `keyword_reply` 方法裡。而為什麼他會跑進這個方法呢?原來是在 `kamigo_controller.rb` 在 `webhook` 方法裡的第 `10` 行的呼叫了 `keyword_reply` 方法。 透過閱讀 `stack trace` 你通常就能夠找到錯誤的根源。 # 在 Heroku 上的資料庫遷移 一如往常: ``` heroku run rake db:migrate ``` 如果你明明已經跑了資料庫遷移程式,但他還是找不到新欄位的話,可以試試看重開 heroku server: ``` heroku restart ``` ### 正確的測試流程 - 把他邀請進群組 1 - 把他邀請進群組 2 - 在群組 1 教他看到 A 要回答 B - 在群組 2 教他看到 A 要回答 C - 在群組 1 說 A 看他是不是回答 B - 在群組 2 說 A 看他是不是回答 C 應該是順利啦~ # 本日重點 - 學會怎麼對已經存在的資料模型加一個欄位 - 學會見人說人話,見鬼說鬼話的本領 - 學會在 Heoku 上除錯的方法

沒有留言: