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)
成功!
失敗的在底下留言,謝謝。
# 本日重點
- 學會了判斷目前的頻道
- 學會了如何根據前後文作出不同的回應
# 接下來
剩沒幾天了,還有很多可以學的,我想知道你們比較想學些什麼?
接下來我們還可以做的事情有這些:
- 現在的程式有點亂了,而且還有些問題,需要整理
- 讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應
- 讓卡米狗能抽籤
- 讓卡米狗能擷取用戶的使用者名稱以及大頭貼
- 讓卡米狗能接收及傳送貼圖
- 讓卡米狗能接收及傳送圖片
- 讓卡米狗能傳送含有按鈕的選單
- 讓卡米狗能查天氣
- 打造一個管理後台
- 讓卡米狗能發公告
- 製作小遊戲,比方說井字遊戲
或者你有想到,但上面沒列出來的也可以。
請在本文留言讓我知道你想學些什麼,可複選。
訂閱:
張貼留言 (Atom)
沒有留言:
張貼留言