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 上除錯的方法
訂閱:
張貼留言 (Atom)
沒有留言:
張貼留言