2018/1/23
只要有心,人人都可以作卡米狗 - 完賽心得
# 參加感想
其實一開始參加的時候是想說反正隨時棄坑都沒關係,至少我有開始過。但沒想到讀者比我預想的還要多,情況有點不受控制,我似乎不得不把質跟量都作出來,不然就會辜負這些讀者。不過也感謝大家的支持,我才能順利完賽,在沒有任何文章存稿的情況下參賽,連我都不相信我能完賽。
# 讀者群的設定
因為卡米狗粉都是沒有接觸資訊領域,不會寫程式的人,所以在我寫文的一開始,就把讀者群設定在電腦只有開過 IE、只安裝過 MMORPG 的等級。要從檔案總管和記事本教起,這件事比我一開始想像中的還要累。在講到任何知識之前,我都得要先想一下,我應該要假設讀者已經學過了嗎?如果我這裡跳過不講,讀者會不會放棄治療,一輩子卡關在這裡呢?還是說我不應該講這麼細節的東西,應該讓讀者用肌肉記憶就好?我是覺得如果我不講,讀者放棄治療的機率很高啦。
對於 iT邦幫忙既有的讀者來說,我設定的讀者群程度可能就太淺了,抱歉占用到你們的版面。不過,我從一開始就不是打算寫給你們(工程師們)看的。
# 關於選題
只要有心,人人都可以作卡米狗,這個選題已經說明了讀者群的設定就是麻瓜。而聊天機器人說穿了就是個只有後端的網站,製作難度肯定低於架網站,我只需要確保每個讀者都懂 HTTP 協定,並且會架 HTTP Server 即可。主要目標是讓讀者看完之後能夠有基礎的網站概念,開始能看得懂工程師寫的技術文章,以及知道遇到問題時要在 GOOGLE 輸入什麼關鍵字的能力。
# 關於文章內容的編排
我首篇先講什麼是聊天機器人,並以卡米狗舉例說明,當然也是為了置入一波卡米狗。
在我作任何教學之前,我會希望讀者能夠先知道為什麼他要學這個,所以我選擇採用從上而下的講解方式,先講最大的框架是由什麼構成,接下來再去認識細節和實作的部分,而每一個實作的部分都是遇到才教。我就是怕我一教難的你們就跑了。
如果我今天第一篇開頭就說,我們要用 sublime、ruby、rails、git、heroku 哦~先安裝吧,然後前面10篇都在安裝,這樣的編排真的有人讀得下去嗎?我很懷疑。我認為要讓讀者能夠在初期就取得巨大的成就感,讀者才會有信心能夠跟著文章走下去。所以我在第三篇就讓讀者建立一個 Line chatbot 帳號,而且可以講一些廢話。後面花了20篇的篇幅在教怎麼作出跟 Line@ 提供的後台一模一樣的東西。
不過這樣的篇排有個缺點,就是不能跳著讀。
# 目錄
大致的切分如下:
### 基本觀念的建立
從聊天機器人帶到 Webhook,再帶到 HTTP 協定以及 Web Server。
[第一天:認識聊天機器人](https://ithelp.ithome.com.tw/articles/10192259)
[第二天:認識卡米狗](https://ithelp.ithome.com.tw/articles/10192575)
[第三天:作一隻最簡單的 Line 聊天機器人](https://ithelp.ithome.com.tw/articles/10192928)
[第四天:認識 Webhook](https://ithelp.ithome.com.tw/articles/10193212)
[第五天:認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441)
[第六天:認識網站](https://ithelp.ithome.com.tw/articles/10193664)
[第七天:認識網頁伺服器](https://ithelp.ithome.com.tw/articles/10193904)
### 開發環境的建立
從 Web Server 帶到 Rails,再帶到 Command Line、Sublime Text
[第八天:安裝 Rails 和認識小黑框](https://ithelp.ithome.com.tw/articles/10194156)
[第九天:作一個最簡單的 Rails 網站](https://ithelp.ithome.com.tw/articles/10194359)
[第十天:認識文字編碼](https://ithelp.ithome.com.tw/articles/10194586)
[第十一天:認識文字編輯器](https://ithelp.ithome.com.tw/articles/10194805)
### HTTP 協定的深入了解
從各個角度了解 HTTP,從瀏覽器發送和接收、也從網站伺服器發送和接收
[第十二天:從瀏覽器認識 HTTP 協定](https://ithelp.ithome.com.tw/articles/10194805)
[第十三天:認識 Ruby 的資料型態](https://ithelp.ithome.com.tw/articles/10195196)
[第十四天:最基本的 Rails 運作流程](https://ithelp.ithome.com.tw/articles/10195380)
[第十五天:從 Rails 認識 HTTP 協定](https://ithelp.ithome.com.tw/articles/10195578)
[第十六天:做一個最簡單的爬蟲](https://ithelp.ithome.com.tw/articles/10195760)
### 發布環境的建立
介紹發布環境,帶到 Heroku 和 Git
[第十七天:怎麼讓別人連到我作好的網站?](https://ithelp.ithome.com.tw/articles/10195920)
[第十八天:發布網站到 Heroku](https://ithelp.ithome.com.tw/articles/10196129)
[第十九天:發布網站到 Heroku (續)](https://ithelp.ithome.com.tw/articles/10196250)
### LINE API 的串接
基礎知識備齊,終於來到正題。讀者設定為一般工程師的話,第一篇大概會從這邊開始寫起。
[第二十天:串接 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10196397)
[第二十一天:讓 Line Bot 回覆訊息](https://ithelp.ithome.com.tw/articles/10196544)
[第二十二天:用 Line Messaging API 實作關鍵字回覆](https://ithelp.ithome.com.tw/articles/10196672)
### 資料庫的操作
缺乏的一塊基礎知識,因為得在這個階段才能感受到為什麼需要資料庫,所以選擇在這個時候才講。寫給工程師看的話,這兩篇大概就略過了。
[第二十三天:認識資料庫](https://ithelp.ithome.com.tw/articles/10196781)
[第二十四天:認識資料庫(續)](https://ithelp.ithome.com.tw/articles/10196895)
### 學習成果的應用
這是大家想看的部分
[第二十五天:卡米狗學說話](https://ithelp.ithome.com.tw/articles/10197013)
[第二十六天:卡米狗推齊](https://ithelp.ithome.com.tw/articles/10197128)
[第二十七天:卡米狗見人說人話,見鬼說鬼話](https://ithelp.ithome.com.tw/articles/10197234)
[第二十八天:建立管理後台](https://ithelp.ithome.com.tw/articles/10197333)
[第二十九天:卡米狗發公告](https://ithelp.ithome.com.tw/articles/10197440)
[第三十天:卡米狗查天氣](https://ithelp.ithome.com.tw/articles/10197544)
# 關於開發環境
選擇在 windows 上開發 rails,而不是選在 macbook 上開發,是因為我認為大多數一般人家裡沒有 macbook,為了降低進入障礙,所以選擇在 windows 上開發,我的卡米狗從一開始就是在 macbook 上開發的,而在我寫文之前,我沒有用過 windows 開發過 rails。選擇用 windows 開發,在後期確實是導致比較多的障礙。不過讀者們會因為這樣而去安裝 linux 或者買一台 macbook 嗎?
# 關於瀏覽量
老實講,最前面的三篇文章我有在卡米狗上面發公告宣傳,成效不錯。但每次發公告,好友人數就掉1%是蠻傷的,應該要作個訂閱機制,針對那些有在 LINE 上訂閱系列文的人,我再每天 PUSH 就好。不過文章寫到一半也沒那個心力去加功能就是了。不過後面有兩篇莫名4千多,我是懷疑有別人在洗我的瀏覽量。
# 最後
在這裡感謝那些留言給我的人,不論你們是提出問題,或回報錯誤,或感謝我,你們都能幫助到我。之後可能會把在這三十篇裡面沒提到的,關於 Line Messaging API 部分也講一講,像是 [imagemap message](https://developers.line.me/en/docs/messaging-api/message-types/#imagemap-messages) 和 [template message](https://developers.line.me/en/docs/messaging-api/message-types/#carousel-template) 這種比較酷炫的功能。
以下開放許願,我考慮有時間的時候再回來講講。
2018/1/18
第三十天:卡米狗查天氣
今天就是最後一天惹,有些事情想跟你們講一下,那就是我們前幾天到底在幹嘛。
以下是一些示意圖,說明我們的 HTTP request 傳遞的路徑。
# 回覆訊息
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIBBLpwmEsBflGznxGYNV4CLgnla3Lh4AhGHHzDm7Y6W5DOjq2wg30F-gixpI6LsMvRfl6HaItAng8R8T5T54c9skSRBFmh72com1rUaAG6rXWbsKN4GT7vn8ebUxUhS3Xb2oGwUCUuZI/s1600/1.jpg)
Line app 指的是手機或PC版的 Line,Line server 在收到訊息後會透過 webhook url 傳遞給我們。接著我們會打 `line.reply_message` 傳訊息給 Line server,最後再由 Line server 傳給 Line app (最後這段可能不是 HTTP request)。
# 發公告
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8T3lFxHxi4rhSMw9ndgSkirez5iQwkfbfGwicUQHLHILsTqN0EseIx5G8bxVRmIc1VK2yhzL0dxMePFlE-B6UcQfjd2jRLBl3bVafCaVEXLmWYQa7fyM0BnONZXXVRZPDenzCHGEtUV0/s1600/2.jpg)
我們透過後台管理介面填入公告訊息,用 `line.push_message` 傳訊息給 Line server。
# 排程公告
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhinS59WbIvMzo1jg4XW6HPlR4xBzN5OM7ZLsywJXMxVoZ-g6WYpjxXM3o4e8vr3P6zdJD2CK9rFHo0t1xravSHin4pQjp-tK2lKwKV_uJUX02mqZtEGuf0rQJHtJ_iplIs4husKWnGbaA/s1600/3.jpg)
有觀眾說想知道鬧鐘怎麼作,這裡再說明一下。
我們會用到 worker 來處理工作排程。首先是先在後台設定預約發訊息,然後將訊息儲存到工作清單,每個工作可以指定執行時間,接著就等時間到,worker 就會用 `line.push_message` 去打 Line server。
# 查天氣
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPy6B-QhMCDjGqgmspWGgDP5A51NwqV1Jc8J4wgIN4vXWRPa8RxuhpYycIlES-nmpsMiVO-c7OiOM9TBeTBjncQtxEc9gXAo_6vzCQ1YYCL1EHUSdTgZS-DIwMteKD1vtfi54mL_fCius/s1600/4.jpg)
查天氣就更複雜了,我們收到查天氣指令後,要先去氣象局取得圖片檔,然後再把圖上傳到 imgur,最後把圖片連結傳回給 Line server。
為什麼不是直接把氣象局的圖片傳給 Line server 呢?因為 Line server 要求圖檔必須是 https 開頭的網址,但是氣象局的圖檔連結卻是 http 開頭。
那為什麼不是我們自己保存圖片就好呢?因為存圖片要占空間跟頻寬,所以我選擇用 imgur 的空間放圖。imgur 有一個蠻好的地方是,你可以直接把圖片網址給他,他就會幫你備份圖片了,所以我們不用真的把圖檔抓回來再上傳到 imgur。
# 查天氣的運作流程
我們作簡單一點,當有人說到`天氣`的時候就傳回一張雷達回波圖。我們需要作的所有事情是:
調查階段:
- 學會怎麼抓到最新的雷達回波圖網址
- 學會怎麼把圖檔弄到 imgur
實作階段:
- 在主程式呼叫查天氣
- 增加一個查天氣函數
- 增加一個取得最新雷達回波圖的函數
- 增加一個上傳圖片到 imgur 的函數
- 傳送圖片到 line 的函數
一步步來吧。
# 學會怎麼抓到最新的雷達回波圖網址
當然,如果我們是用瀏覽器下載,那麼很簡單直接網頁打開`右鍵`->`另存圖片`就載好了。可是我們是要用程式去載圖,不是人工載圖。
所以我們要用程式去開啟網頁,然後從網頁原始碼裡面找到圖片連結就行了。
先開這個網頁:[http://www.cwb.gov.tw/V7/observe/radar/](http://www.cwb.gov.tw/V7/observe/radar/)
然後按下 `Ctrl`+`U`,就可以看到網頁原始碼了,把他認真的讀完之後會發現第 234~237 行很可疑,點進去看就會發現全都是圖檔連結,像這樣:[http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js](http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js)。
要能發現第 234~237 行很可疑,你必須要能看懂大部分的 html 跟 js,所以你得學會 html 跟 js。
如果你還沒學過 html 的話,可以參考看看:[深入淺出立即上手的 HTML 網頁設計](https://5xruby.tw/talks/css-html-2018-1)
如果你還沒學過 js 的話,也可以參考看看:[JavaScript & jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)
```
var HDRadar_1000_n_val=new Array(
new Array("2018/01/18 01:20","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png"),
new Array("2018/01/18 01:10","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180110.png"),
new Array("2018/01/18 01:00","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180100.png"),
new Array("2018/01/18 00:50","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180050.png"),
...
```
這是 js 程式碼,我們需要的部分在第二行後半段:`/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png`,這是網頁路徑,省略了網域的寫法。
把網域加回去就會是 [http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png](http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png):
![](http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png)
這就是我們要的圖片連結。
### 小結
抓 [http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js](http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js) 的原始碼,然後取出第二行的網頁路徑,最後在前面補上 `http://www.cwb.gov.tw` 就會是我們要的網址。
# 學會怎麼把圖檔弄到 imgur
imgur 有提供 api,這是說明文件:[https://apidocs.imgur.com/#4b8da0b3-3e73-13f0-d60b-2ff715e8394f](https://apidocs.imgur.com/#4b8da0b3-3e73-13f0-d60b-2ff715e8394f)。
使用 api 需要 Client-ID,這東西就跟 Line channel secret 那些東西差不多。
你可以透過這個網址:[https://api.imgur.com/oauth2/addclient](https://api.imgur.com/oauth2/addclient) 取得你的 Client-ID。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjer94avY65FjgMHD2HDhxK5GuUvm2ribzg_WSWlw4hx7azCULh3E7N81JzOymhCHracAx72jWM8Ue5oSHaS9saoU1wXpPAfAnCkJmkk5DsSUImBZnp0BsibSAnVK15-_ntYHA9wtJoXU0/s1600/6.jpg)
照著填就可以。
# 小結
透過使用 imgur 提供的 api,我們可以很容易就上傳圖片到 imgur。
接下來是實作階段的部分。
# 在主程式呼叫查天氣
```
def webhook
# 查天氣
reply_image = get_weather(received_text)
# 有查到的話 後面的事情就不作了
unless reply_image.nil?
# 傳送訊息到 line
response = reply_image_to_line(reply_image)
# 回應 200
head :ok
return
end
# 紀錄頻道
Channel.find_or_create_by(channel_id: channel_id)
# 學說話
reply_text = learn(channel_id, received_text)
# 關鍵字回覆
reply_text = keyword_reply(channel_id, 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
```
我在最前面加入了這段程式碼:
```
# 查天氣
reply_image = get_weather(received_text)
# 有查到的話 後面的事情就不作了
unless reply_image.nil?
# 傳送訊息到 line
response = reply_image_to_line(reply_image)
# 回應 200
head :ok
return
end
```
我們要作一個查天氣函數 `get_weather` 如果輸入的文字包含`天氣`,就傳回 https 的雷達回波圖網址,然後就將圖片傳回給 line,這裡因為之前都是傳文字而已,所以還要多作一個函數 `reply_image_to_line` 來傳圖片。
# 增加一個查天氣函數
```
def get_weather(received_text)
return nil unless received_text.include? '天氣'
imgur(get_weather_from_cwb)
end
```
第一行是說如果輸入的文字不包含天氣,就傳回 nil。
第二行呼叫了兩個函數,第一個函數是 `get_weather_from_cwb`,這是取得雷達回波圖的函數,會得到一個網址,再把這個網址傳給 `upload_to_imgur` 這個上傳圖片到 imgur 的函數。
# 增加一個取得最新雷達回波圖的函數
在[第十六天:做一個最簡單的爬蟲](ttps://ithelp.ithome.com.tw/articles/10195760)學到的在 rails 發 HTTP request 跟在[第二十五天:卡米狗學說話](https://ithelp.ithome.com.tw/articles/10197013)學到的字串處理又要派上用場了,就跟你說前面的文章都是在打基礎吧,漏掉一篇你就做不出來了。
```
def get_weather_from_cwb
uri = URI('http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js')
response = Net::HTTP.get(uri)
start_index = response.index('","') + 3
end_index = response.index('"),') - 1
"http://www.cwb.gov.tw" + response[start_index..end_index]
end
```
前兩行就是第十六天講過的,後三行就是第二十五天講過的。比較難懂的可能會是第三行跟第四行,先看一下這張圖:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0ou1oNmbu3Vsu744XKQ6rLhJwcb5HOUktAAbRDXY6UAAfZZg5dOb4hxcZBPh47j0gk6LL_ecyO7bRN8lW9ooM9vnoL61k3dPnOAdCwdmyoN0_gnz-a0j3PzYku3GlHlKE7o-DB9Ed_7U/s1600/5.jpg)
總而言之就是網址的開頭前面是 `","` 後面是 `"),` 如果你有學過 js 應該就會知道,這個開頭跟結尾應該是不會錯的,所以我們決定取出介於這中間的字。
這行是在抓起點:
```
start_index = response.index('","') + 3
```
這是在抓終點:
```
end_index = response.index('"),') - 1
```
# 增加一個上傳圖片到 imgur 的函數
```
def upload_to_imgur(image_url)
url = URI("https://api.imgur.com/3/image")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["authorization"] = 'Client-ID be2d83405627ab8'
request.set_form_data({"image" => image_url})
response = http.request(request)
json = JSON.parse(response.read_body)
begin
json['data']['link'].gsub("http:","https:")
rescue
nil
end
end
```
我們設定好 request header 和 request body 之後打一個 post request 出去,他會返回一個 json,接著我作了 json 的解析,並且在解析失敗時傳回 nil,確保程式不會隨意掛點。
```
request["authorization"] = 'Client-ID be2d83405627ab8'
```
這行是要填入你自己的 Client-ID,`be2d83405627ab8` 是我亂打的。
# 傳送圖片到 line 的函數
```
# 傳送圖片到 line
def reply_image_to_line(reply_image)
return nil if reply_image.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: "image",
originalContentUrl: reply_image,
previewImageUrl: reply_image
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
其實跟傳文字幾乎一樣,只差在 message 裡面不一樣而已。
# 上傳實測
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS1WcxYPIl9VoOutCXhiiZtKxCbxAf1IR3fhzmnYiJ6LJU190f9GFfZOEFf9_xNnjQTL6qr0_odQiqQ0OXxrg6EdRmoXY3LQIJdODgq2C1zH1VylSCSnnOZ6XXzwZMyktsmpYnYkiVPf0/s1600/7.jpg)
成功!
# 本日重點
- 學會抓雷達回波圖
- 學會用 imgur api
- 學會傳圖片到 line
- 想要學會作爬蟲,就要學會 html 跟 js
- 如果你還沒學過 html 的話,可以參考看看:[深入淺出立即上手的 HTML 網頁設計](https://5xruby.tw/talks/css-html-2018-1)
- 如果你還沒學過 js 的話,也可以參考看看:[JavaScript & jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)
如果你原本是完全不會寫程式,你從第一篇一直看到這篇,最後有作出東西的話,請在底下留言:「感恩卡米,讚嘆卡米」,讓我能證明`只要有心,人人都可以作卡米狗`是真的。
2018/1/17
Rails - Windows 上會遇到的 LoadError (cannot load such file -- bcrypt_ext) 問題
# 什麼時候會遇到這個問題?
當你使用任何需要加密功能的套件時,比方說 Devise。
# 成因
安裝了不能在 windows 下正常執行的 bcrypt 套件。
# 解法
先解除安裝所有 bcrypt
```
gem uninstall bcrypt-ruby
gem uninstall bcrypt
```
再安裝正確版本
```
gem install bcrypt --platform=ruby
```
你的 Gemfile 應該加入這行
```
gem 'bcrypt', '~> 3.1.11'
```
# 參考連結
[https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799](https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799)
第二十九天:卡米狗發公告
今天我們要作的是主動傳訊息的功能。
目前我們用到的都只是回覆訊息的功能:
# 認識 Push Message API
```
# 傳送訊息到 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
```
上面這個函數是我們之前寫好的 `reply_to_line` 函數,裡面的最後一行:
```
line.reply_message(reply_token, message)
```
這是在呼叫 line 提供給我們的回覆訊息函數,而 line 也有提供讓我們主動發訊息的函數:
```
response = line.push_message(channel_id, message)
```
我們需要傳遞 channel_id,告訴 Line 誰應該收到這個訊息,channel_id 就是 userId, groupId 或 roomId。所以我們需要一個資料模型去保存所有頻道的 channel_id。
文件參考在這裡:[https://developers.line.me/en/docs/messaging-api/reference/#send-push-message](https://developers.line.me/en/docs/messaging-api/reference/#send-push-message)
# 保存所有頻道
### 建立資料模型
```
rails g model channel channel_id
```
建立一個資料表叫作 channel,裡面有個欄位叫作 channel_id。
### 資料庫遷移
```
rails db:migrate
```
bj4
### 儲存頻道
在主程式中加入一行:
```
Channel.create(channel_id: channel_id)
```
如果你覺得是這樣寫,那你就錯了,因為這樣會導致相同的資料會一直被存進去,到時候你發公告,同一個人就會收到超多次。
```
Channel.find_or_create_by(channel_id: channel_id)
```
先看有沒有相同的資料,如果已經有資料的話就不寫入。如果沒有資料才作寫入。這邊有詳細的說明:[https://rails.ruby.tw/active_record_querying.html#find-or-create-by](https://rails.ruby.tw/active_record_querying.html#find-or-create-by)
加入後的主程式長這樣:
```
def webhook
# 紀錄頻道
Channel.find_or_create_by(channel_id: channel_id)
# 學說話
reply_text = learn(channel_id, received_text)
# 關鍵字回覆
reply_text = keyword_reply(channel_id, 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
```
接下來要作一個後台網頁去發這個公告。我們會需要兩個 Action,一個 get new Action 用來顯示發公告的後台頁面,另一個 post create Action 用來接收發公告訊息的請求。
# 管理後台
這次我們手動新增,不使用產生器。
### 加 Route
修改 `config/routes.rb`,新增一行:
```
resources :push_messages, only: [:new, :create]
```
這是加入一組資源,但我們只使用其中的 new 和 create。
### 加 Controller
在 `app/controllers` 資料夾下建立一個叫作 `push_messages_controller.rb` 的檔案:
```
class PushMessagesController < ApplicationController
before_action :authenticate_user!
# GET /push_messages/new
def new
end
# POST /push_messages
def create
end
end
```
我們檢查使用者必須先登入,然後開了兩個空的 Action,之後再回頭來改。
### 加 View
我們要在 `app/views/push_messages` 下新增一個檔案 `new.html.erb`。
這是 `new.html.erb` 所需要的全部程式碼:
```
<%= form_with(url: '/push_messages', local: true) do |form| %>
<%= text_area_tag 'text' %>
<%= submit_tag "送出" %>
<% end %>
```
一個表單的開始是 `<%= form_with ..... do ... %>`,結束是 `<% end %>`。表單預設是用 post 方法,所以就不用特別寫出來。
`<%= text_area_tag 'text' %>` 是輸入文字框。
`<%= submit_tag "送出" %>` 則是送出按鈕。
如果要了解更多的話可以參考:[Action View 表單輔助方法](https://rails.ruby.tw/form_helpers.html)
### 改 Controller
我們在接收到請求之後要作發訊息的動作:
```
def create
text = params[:text]
Channel.all.each do |channel|
push_to_line(channel.channel_id, text)
end
redirect_to '/push_messages/new'
end
```
`text = params[:text]` 這是取得剛剛在輸入文字框填的文字
`Channel.all.each do |channel|` ... `end` 這段是指我們想要對每一個 channel 作一些事情。
`push_to_line(channel.channel_id, text)` 這是說我們要主動發訊息 `text` 給頻道 `channel.channel_id`,這個函數我們待會才會寫。
### push_to_line
```
# 傳送訊息到 line
def push_to_line(channel_id, text)
return nil if channel_id.nil? or text.nil?
# 設定回覆訊息
message = {
type: 'text',
text: text
}
# 傳送訊息
line.push_message(channel_id, message)
end
```
長得跟之前的 `reply_to_line` 有 87% 像,就不解釋了。
### 對一下程式碼
完整的 `push_messages_controller.rb` 應該長這樣:
```
require 'line/bot'
class PushMessagesController < ApplicationController
before_action :authenticate_user!
# GET /push_messages/new
def new
end
# POST /push_messages
def create
text = params[:text]
Channel.all.each do |channel|
push_to_line(channel.channel_id, text)
end
redirect_to '/push_messages/new'
end
# 傳送訊息到 line
def push_to_line(channel_id, text)
return nil if channel_id.nil? or text.nil?
# 設定回覆訊息
message = {
type: 'text',
text: text
}
# 傳送訊息
line.push_message(channel_id, 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
end
```
# 發布和測試
自己試試看,你們需要練習自己解決卡關,要養成看 log 的習慣。
# 關於怎麼做鬧鐘
你需要把主動發訊息這件事情加入排程,請參考:[Active Job 基礎](https://rails.ruby.tw/active_job_basics.html#%E4%BB%BB%E5%8B%99%E6%8E%92%E7%A8%8B) 以及 [Delayed Job (DJ)](https://devcenter.heroku.com/articles/delayed-job)。
自己摸這個要有花上一週的心理準備,加油加油加油,你是最棒的,耶!
# 本日重點
- 學會記錄頻道
- 學會主動發訊息
- 學會寫 view 的表單
沒意外的話,明天就講怎麼查天氣。
2018/1/16
第二十八天:建立管理後台
應觀眾要求,今天我們作一個管理後台,讓我們可以在網頁上管理關鍵字。
在開始之前,先大概說明一下今天要學習的範圍有哪些:
- 網頁的呈現需要使用 HTML 和 CSS
- 既然是後台,就要作登入功能
我們作的網站到目前為止沒有碰過任何的 HTML 和 CSS,突然要寫個管理後台也許會很吃力。不過還好是作後台,不需要多美觀。
# 使用產生器製作後台
幸好 Rails 有一個內建指令直接生成網頁,不一定要自己寫。
指令是 `rails generate scaffold 資料模型名稱 和欄位們`
```
rails g scaffold keyword_mapping channel_id keyword message --skip
```
後面的 `--skip` 是指定當發生衝突時應該略過。衝突的意思是指 rails 想新增一個檔案,剛好在目錄裡已經有個同名的檔案。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails g scaffold keyword_mapping channel_id keyword message --skip
invoke active_record
skip db/migrate/20180115144538_create_keyword_mappings.rb
identical app/models/keyword_mapping.rb
invoke test_unit
identical test/models/keyword_mapping_test.rb
skip test/fixtures/keyword_mappings.yml
invoke resource_route
route resources :keyword_mappings
invoke scaffold_controller
create app/controllers/keyword_mappings_controller.rb
invoke erb
create app/views/keyword_mappings
create app/views/keyword_mappings/index.html.erb
create app/views/keyword_mappings/edit.html.erb
create app/views/keyword_mappings/show.html.erb
create app/views/keyword_mappings/new.html.erb
create app/views/keyword_mappings/_form.html.erb
invoke test_unit
create test/controllers/keyword_mappings_controller_test.rb
invoke helper
create app/helpers/keyword_mappings_helper.rb
invoke test_unit
invoke jbuilder
create app/views/keyword_mappings/index.json.jbuilder
create app/views/keyword_mappings/show.json.jbuilder
create app/views/keyword_mappings/_keyword_mapping.json.jbuilder
invoke test_unit
create test/system/keyword_mappings_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/keyword_mappings.coffee
invoke scss
create app/assets/stylesheets/keyword_mappings.scss
invoke scss
create app/assets/stylesheets/scaffolds.scss
D:\只要有心,人人都可以作卡米狗\ironman>
```
以下說明到底生成了什麼東西。
# 生成 Routes
他會在 `config/routes.rb` 生成一個 resource:
```
resources :keyword_mappings
```
這是資源(resource),提供一個資源的存取所需要的網址和 Controller 的對應。
這行會生成 8 組網址與 7 個 Controller Action 的對應,可以使用 `rails routes` 觀察:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails routes
Prefix Verb URI Pattern Controller#Action
keyword_mappings GET /keyword_mappings(.:format) keyword_mappings#index
POST /keyword_mappings(.:format) keyword_mappings#create
new_keyword_mapping GET /keyword_mappings/new(.:format) keyword_mappings#new
edit_keyword_mapping GET /keyword_mappings/:id/edit(.:format) keyword_mappings#edit
keyword_mapping GET /keyword_mappings/:id(.:format) keyword_mappings#show
PATCH /keyword_mappings/:id(.:format) keyword_mappings#update
PUT /keyword_mappings/:id(.:format) keyword_mappings#update
DELETE /keyword_mappings/:id(.:format) keyword_mappings#destroy
kamigo_eat GET /kamigo/eat(.:format) kamigo#eat
kamigo_request_headers GET /kamigo/request_headers(.:format) kamigo#request_headers
kamigo_request_body GET /kamigo/request_body(.:format) kamigo#request_body
kamigo_response_headers GET /kamigo/response_headers(.:format) kamigo#response_headers
kamigo_response_body GET /kamigo/response_body(.:format) kamigo#show_response_body
kamigo_sent_request GET /kamigo/sent_request(.:format) kamigo#sent_request
kamigo_webhook POST /kamigo/webhook(.:format) kamigo#webhook
```
7 個 Action 分別為 index, create, new, edit, show, update, destroy,接下來說明各個 Action 的功能:
以下屬於 GET request,這些都是網頁:
- index:列表頁
- new:新增資料頁
- show:檢視資料頁
- edit:編輯資料頁
以下非 GET request,都是請求資料變更:
- create:請求新增資料
- update:請求更新資料
- destroy:請求刪除資料
# 生成 Controller
生成了一個 Controller 在:`app/controllers/keyword_mappings_controller.rb`。7 個對應的 Action 都寫好了,這裡就不多介紹。
# 生成 View
生成了一整個資料夾的 View,其中最重要的 4 個:
```
app/views/keyword_mappings/index.html.erb
app/views/keyword_mappings/edit.html.erb
app/views/keyword_mappings/show.html.erb
app/views/keyword_mappings/new.html.erb
```
這就是那些 GET request 會用到的網頁檔,也都寫好了。
# 實測
既然都寫好了就來試用看看,先執行網頁伺服器:
```
rails s
```
然後開啟網頁 [http://localhost:3000/keyword_mappings](http://localhost:3000/keyword_mappings):
### index 列表頁
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEist8IdgbyY3iAIGlOyofKz1eCnlRALx4SRUXGxlWhdzor1anPL8AvYbf9NuWaOJcmEr3pZLcr0eqLOJG_QNoxpmzsQPQNm5RDYPVXUukTpM4A7TGRsKqx_U_Gbh57VDIKGNLtBt0oIMNM/s1600/1.jpg)
### new 新增資料頁
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAQEbyfGqL9bOS5Jii-T2ovBPGBvgninU-HovOPbFaU3sQcKcaRWII3sPjce9droyLKCJEzKbaE9Bsv2n2JknSVhXTF99JMZmyFHiyXgYb6HamLGq5pm_D420ZdboEkF4blIJYFK5Whb0/s1600/2.jpg)
隨便亂填:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-1GgB1iT6XwiUq92wHqLCYcn5gbDaS6AJqhO3qHQT3BubyklAHFTq0Q3bz-lNH6dOiZxW5Kejc5BYcT2yHKl_sxSobzHpE1RRGTWL3Yq5T2CaOgyPt-yFoVjAz8Rzox-Y3SKWrfSIaCM/s1600/3.jpg)
### show 檢視資料頁
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCEGVY_kVAgDVOCwh5BSo9g3DEpfqcl9OUG3l3cN-NKNDHQe15EfGIndU3xtydKWlb4SoY3t4pwsmXf7F8aVovY1G16_ft24cIzhMXDSMle8UmjxU8EmK6W7KubR7b7l7-FmoW9rPe1KQ/s1600/4.jpg)
所以其實不考慮美觀性的話,其實後台只要一個指令就完成了。
# 建立登入功能
使用知名套件 [devise](https://github.com/plataformatec/devise) 來作。
我相信現在的你如果沒有英文閱讀障礙,應該已經能看懂這個[使用說明](https://github.com/plataformatec/devise#getting-started)。
總而言之先在 Gemfile 加這行:
```
gem 'devise'
```
然後在小黑框打 `bundle install` 安裝套件。
裝好之後使用 devise 提供的產生器指令進行初始化: `rails generate devise:install`。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails generate devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Some setup you must do manually if you haven't yet:
1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
irb(main):002:0>
```
看到倒數第二行:
```
=> #
```
就表示建立好帳號了。
# 線上實測
[https://people-all-love-kamigo.herokuapp.com/keyword_mappings](https://people-all-love-kamigo.herokuapp.com/keyword_mappings):
大家可以用我的帳號登入看看。
帳號:kamigo.service@gmail.com
密碼:kamigo
# 本日重點
- 學會使用 scaffold
- 學會作登入系統
你們可以透過閱讀 scaffold 產生出來的程式碼來學習 HTML 和 Controller Action 的寫法。這跟學英文一樣,看到不懂的單字就 Google,這單字量還比英文少超多,大概 100~200 個字而已。
明天講怎麼發公告。
<%= notice %>
<%= alert %>
4. You can copy Devise views (for customization) to your app by running: rails g devise:views =============================================================================== D:\只要有心,人人都可以作卡米狗\ironman> ``` 他說有幾個步驟產生器沒搞頭,必須手動進行。我們先不管他,等到真正出問題再回頭來解決。 使用產生器產生用戶資料模型: ``` rails generate devise user ``` ``` D:\只要有心,人人都可以作卡米狗\ironman>rails generate devise user invoke active_record create db/migrate/20180115152537_devise_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb route devise_for :users D:\只要有心,人人都可以作卡米狗\ironman> ``` 跟剛剛的 scaffold 差不多,該生的都生好了。 註冊頁:[http://localhost:3000/users/sign_up](http://localhost:3000/users/sign_up) 登入頁:[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in) # 關閉註冊功能 我們要將註冊功能關閉,如果大家都能註冊,那還要後台幹嘛? 在 `app/models/user.rb`: ``` class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end ``` 刪除 `:registerable,`: ``` class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable end ``` # 登入後才能管理關鍵字 我們希望只有登入後的人才能進入管理關鍵字的頁面。 在 `app/controllers/keyword_mappings_controller.rb` 加入: ``` before_action :authenticate_user! ``` 看起來像這樣: ``` class KeywordMappingsController < ApplicationController before_action :authenticate_user! before_action :set_keyword_mapping, only: [:show, :edit, :update, :destroy] ...下略 ``` 這時候開啟網址:[http://localhost:3000/keyword_mappings](http://localhost:3000/keyword_mappings),就會因為尚未登入,而被引導至登入頁。 # 發布流程 - 上傳程式碼 - Heroku 上的資料庫遷移 # 關閉了註冊功能後要怎麼新增自己的帳號? 使用 `rails console` 連上去新增帳號: ``` heroku run rails console ``` 連上後會是 `rails console` 的樣子: ``` D:\只要有心,人人都可以作卡米狗\ironman>heroku run rails console Running rails console on people-all-love-kamigo... up, run.2165 (Free) Loading production environment (Rails 5.1.4) irb(main):001:0> ``` 寫一行程式碼新增資料: ``` User.create(email:'kamigo.service@gmail.com', password:'kamigo') ``` 會有一些 SQL 的訊息: ``` irb(main):001:0> User.create(email:'kamigo.service@gmail.com', password:'kamigo') D, [2018-01-15T15:42:28.307402 #4] DEBUG -- : (6.0ms) BEGIN D, [2018-01-15T15:42:28.313291 #4] DEBUG -- : User Exists (2.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "kamigo.service@gmail.com"], ["LIMIT", 1]] D, [2018-01-15T15:42:28.317361 #4] DEBUG -- : SQL (1.9ms) INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["email", "kamigo.service@gmail.com"], ["encrypted_password", "$2a$11$EyR.yuDYI3J2s9/Q8Etk5evQzsz2bGAPdvdcr.xmFQbzYbBPQk/kK"], ["created_at", "2018-01-15 15:42:28.313883"], ["updated_at", "2018-01-15 15:42:28.313883"]] D, [2018-01-15T15:42:28.320139 #4] DEBUG -- : (2.0ms) COMMIT => #2018/1/15
Rails - 多檔上傳
假設有一個 Controller 叫做 `imgur`,大概是這樣:
```
rails g scaffold imgur pictures
```
而 Controller 內的 params 只允許傳遞 pictures 陣列:
```
def imgur_params
params.require(:imgur).permit(pictures: [])
end
```
一個最基本的檔案上傳表單長這樣:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures %>
<%= f.submit %>
<% end %>
```
會因為 pictures 只吃陣列的關係,就傳不進去。如果想要上傳多個檔案,要加上 `multiple: true`:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures, multiple: true %>
<%= f.submit %>
<% end %>
```
可以使用多個 f.file_field 來做上傳:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures, multiple: true %>
<%= f.file_field :pictures, multiple: true %>
<%= f.submit %>
<% end %>
```
如果想要改用 `file_field_tag` 的話,就必須在 `form_for` 加上 `multipart: true`:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]" %>
<%= f.submit %>
<% end %>
```
加上 [] 之後,即使只傳一個檔也能通過 `params.permit`。
如果想要多檔上傳:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= f.submit %>
<% end %>
```
也可以這樣:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]" %>
<%= file_field_tag "imgur[pictures][]" %>
<%= f.submit %>
<% end %>
```
或者這樣:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= f.submit %>
<% end %>
```
第二十七天:卡米狗見人說人話,見鬼說鬼話
在[第二天:認識卡米狗](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 上除錯的方法
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)
成功!
失敗的在底下留言,謝謝。
# 本日重點
- 學會了判斷目前的頻道
- 學會了如何根據前後文作出不同的回應
# 接下來
剩沒幾天了,還有很多可以學的,我想知道你們比較想學些什麼?
接下來我們還可以做的事情有這些:
- 現在的程式有點亂了,而且還有些問題,需要整理
- 讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應
- 讓卡米狗能抽籤
- 讓卡米狗能擷取用戶的使用者名稱以及大頭貼
- 讓卡米狗能接收及傳送貼圖
- 讓卡米狗能接收及傳送圖片
- 讓卡米狗能傳送含有按鈕的選單
- 讓卡米狗能查天氣
- 打造一個管理後台
- 讓卡米狗能發公告
- 製作小遊戲,比方說井字遊戲
或者你有想到,但上面沒列出來的也可以。
請在本文留言讓我知道你想學些什麼,可複選。
2018/1/13
第二十五天:卡米狗學說話
卡米狗的學說話指令,最早期的語法設計是`卡米狗學說話;關鍵字;回覆`。用兩個半形分號作為分隔符號。為什麼選擇用分號作為分隔符號呢?因為我們的分隔符號不能出現在關鍵字或回覆內,所以要挑一個比較少人用的符號。
我們必須讓學說話功能的優先順序高於關鍵字回覆,這樣才能確保學說話指令不會被關鍵字覆蓋。
# 修改主程式
主程式:
```
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
```
第一行:
```
reply_text = learn(received_text)
```
`learn` 是一個待會要寫的新函數,如果使用者說出一句話剛好符合學說話語法,那麼就回應`好哦~好哦~`並儲存結果。如果使用者說出一句話不符合學說話語法,就傳回 `nil`。 `nil` 是代表空值的意思。
第二行:
```
reply_text = keyword_reply(received_text) if reply_text.nil?
```
如果 `reply_text` 是空值的話才進行關鍵字回覆的判斷。這樣就能確保學說話指令優先於關鍵字回覆。
我們要判斷輸入的文字開頭是不是`卡米狗學說話;`,要做到這件事情,我們需要學一點字串操作。
# 字串操作
這是一個字串:
```
'ABCDEF'
=> "ABCDEF"
```
### 字串的切割
這是字串的第一個字:
```
'ABCDEF'[0]
=> "A"
```
這是字串的第二個字:
```
'ABCDEF'[1]
=> "B"
```
以此類推,
A,B,C,D,E,F
分別對應到:
0,1,2,3,4,5
這是字串的倒數第一個字
```
'ABCDEF'[-1]
=> "F"
```
這是字串的倒數第二個字
```
'ABCDEF'[-2]
=> "E"
```
以此類推,
F,E,D,C,B,A
分別對應到:
-1,-2,-3,-4,-5,-6
另外,你可以透過 Range 取得一個區間。
```
'ABCDEF'[0..1]
=> "AB"
```
```
'ABCDEF'[0..3]
=> "ABCD"
```
```
'ABCDEF'[3..-1]
=> "DEF"
```
# 字串的查詢
想知道字串中的 `A` 出現在哪裡:
```
'ABCDEF'.index('A')
=> 0
```
想知道字串中的 `B` 出現在哪裡:
```
'ABCDEF'.index('B')
=> 1
```
想知道字串中的 `C` 出現在哪裡:
```
'ABCDEF'.index('C')
=> 2
```
找不到的情形會傳回 `nil`:
```
'ABCDEF'.index('G')
=> nil
```
# 字串的相等
判斷兩個字串是否相等:
```
'A' == 'A'
=> true
```
```
'卡米狗學說話' == '卡米狗學說話'
=> true
```
```
'A' == 'B'
=> false
```
學會以上三個技巧,就能夠解決大部分的問題。
# 學說話
現在我們要開始寫學說話函數,從空函數開始。
```
# 學說話
def learn(received_text)
end
```
利用以上三個技巧,我們可以先取得前面七個字,看看是不是等於`卡米狗學說話;`,如果是的話,在後面的字串中找到分號作為分隔點。
先檢查開頭的字是不是`卡米狗學說話;`:
```
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
end
```
`unless` 是 `if` 的相反,`unless` 是`除非`的意思。
除非前面七個字是`卡米狗學說話;`,不然就傳回 `nil`。
再來就是取得剩下來的字,以及找到第二個分號。
```
# 學說話
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?
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)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
```
要改成從資料庫查詢,其實昨天已經寫好了:
```
# 關鍵字回覆
def keyword_reply(received_text)
mapping = KeywordMapping.where(keyword: received_text).last
if mapping.nil?
nil
else
mapping.message
end
end
```
將查詢結果存到 `mapping` 變數中,然後檢查有沒有查到東西,如果有才傳回。
這裡可以加入一點浪漫:
```
# 關鍵字回覆
def keyword_reply(received_text)
KeywordMapping.where(keyword: received_text).last&.message
end
```
如果 `&.` 的前面是 `nil`,那他就不會做後面的事,直接傳回 `nil`。
到這裡算是開發完成,可以上傳程式碼了。
# 對一下程式碼
你的程式碼應該長得差不多像這樣:
```
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?
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
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
```
上傳後,我們還有一些工作要做:
# postgresql 版本的指定
如果你在 `heroku logs -t` 上面看到這個錯誤訊息:
```
2018-01-12T18:30:39.687847+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 18506 -e production`
2018-01-12T18:30:45.609275+00:00 app[web.1]: /app/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/connection_specification.rb:188:in `rescue in spec': Specified 'postgresql' for database adapter, but the gem is not loaded. Add `gem 'pg'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord). (Gem::LoadError)
```
請將你的 Gemfile 修改一下,原本是:
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg'
end
```
改為
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg', '~> 0.21.0'
end
```
由於三天前 pg 發布了[新版本](https://bitbucket.org/ged/ruby-pg/commits/tag/v1.0.0),而新版本似乎有點問題,所以我們需要指定安裝[穩定的版本](https://bitbucket.org/ged/ruby-pg/commits/tag/v0.21.0)。如果我們不指定版本,就會安裝到有問題的最新版。
# 安裝 Heroku 上的資料庫
使用 `heroku addons:create heroku-postgresql:hobby-dev` 指令弄一台免費的資料庫來玩玩。
```
D:\只要有心,人人都可以作卡米狗\ironman>heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on people-all-love-kamigo... free
Database has been created and is available
! This database is empty. If upgrading, you can transfer
! data from another database with pg:copy
Created postgresql-concave-22896 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
D:\只要有心,人人都可以作卡米狗\ironman>
```
# 進行在 Heroku 上的資料庫遷移
在我們的小黑框輸入 `heroku run rake db:migrate`:
```
D:\只要有心,人人都可以作卡米狗\ironman>heroku run rake db:migrate
Running rake db:migrate on people-all-love-kamigo... up, run.8915 (Free)
D, [2018-01-12T18:43:37.665151 #4] DEBUG -- : (1852.2ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL PRIMARY KEY)
D, [2018-01-12T18:43:38.188458 #4] DEBUG -- : (491.4ms) CREATE TABLE "ar_internal_metadata" ("key" character varying NOT NULL PRIMARY KEY, "value" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
D, [2018-01-12T18:43:38.194442 #4] DEBUG -- : (2.3ms) SELECT pg_try_advisory_lock(8162367372296191845)
D, [2018-01-12T18:43:39.009656 #4] DEBUG -- : (2.2ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
I, [2018-01-12T18:43:39.012007 #4] INFO -- : Migrating to CreateKeywordMappings (20180110181744)
D, [2018-01-12T18:43:39.015455 #4] DEBUG -- : (0.8ms) BEGIN
== 20180110181744 CreateKeywordMappings: migrating ============================
-- create_table(:keyword_mappings)
D, [2018-01-12T18:43:39.833168 #4] DEBUG -- : (815.6ms) CREATE TABLE "keyword_mappings" ("id" bigserial primary key, "keyword" character varying, "message" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
-> 0.8170s
== 20180110181744 CreateKeywordMappings: migrated (0.8174s) ===================
D, [2018-01-12T18:43:39.853181 #4] DEBUG -- : SQL (6.3ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20180110181744"]]
D, [2018-01-12T18:43:39.861444 #4] DEBUG -- : (5.7ms) COMMIT
D, [2018-01-12T18:43:39.880216 #4] DEBUG -- : ActiveRecord::InternalMetadata Load (2.8ms) SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2 [["key", "environment"], ["LIMIT", 1]]
D, [2018-01-12T18:43:39.896978 #4] DEBUG -- : (1.1ms) BEGIN
D, [2018-01-12T18:43:39.899766 #4] DEBUG -- : SQL (1.0ms) INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key" [["key", "environment"], ["value", "production"], ["created_at", "2018-01-12 18:43:39.897705"], ["updated_at", "2018-01-12 18:43:39.897705"]]
D, [2018-01-12T18:43:39.902418 #4] DEBUG -- : (1.8ms) COMMIT
D, [2018-01-12T18:43:39.903709 #4] DEBUG -- : (0.8ms) SELECT pg_advisory_unlock(8162367372296191845)
D:\只要有心,人人都可以作卡米狗\ironman>
```
# 進行實測
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCC42Z8ZcN5KoIt5415XPVjw94yXDjb6-IwVnTVXVf6AbFQx6QY_ZDK1oVs-RAKsF9eWf5ryyuEphUJ0yEcB_8ClWwrM8PMMSVdYTt-uGD4gNSPiuUOnaJHPlVPVxYAltuV8GzJNgL3NE/s1600/1.jpg)
順利~不順利的人請在底下留言並附上不順利的截圖,謝謝。
# 本日重點
- 學習了字串操作
- 學習了 Heroku 上的資料庫建置
- 做出了學說話和觸發說話
明天講推齊功能。
2018/1/11
第二十四天:認識資料庫(續)
昨天我們講到資料模型產生器的用法:
```
rails generate model keyword_mapping keyword message
```
會產生兩個我們需要的檔案:
- 資料庫遷移檔:`db/migrate/20180110181744_create_keyword_mappings.rb`
- 資料模型:`app/models/keyword_mapping.rb`
其中,資料庫遷移檔就像是一張對資料庫施法的卷軸設計圖,可以用來幫資料庫升級。
那要怎麼升級呢?
# 資料庫遷移
使用 `rails db:migrate` 指令就會進行資料庫升級。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate
== 20180110181744 CreateKeywordMappings: migrating ============================
-- create_table(:keyword_mappings)
-> 0.4916s
== 20180110181744 CreateKeywordMappings: migrated (0.4927s) ===================
D:\只要有心,人人都可以作卡米狗\ironman>
```
還可以用 `rails db:rollback` 降級:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:rollback
== 20180110181744 CreateKeywordMappings: reverting ============================
-- drop_table(:keyword_mappings)
-> 0.5057s
== 20180110181744 CreateKeywordMappings: reverted (0.5211s) ===================
D:\只要有心,人人都可以作卡米狗\ironman>
```
可以用 `rails db:migrate:status` 查看目前等級。這是升級前:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate:status
database: D:/只要有心,人人都可以作卡米狗/ironman/db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
down 20180110181744 Create keyword mappings
D:\只要有心,人人都可以作卡米狗\ironman>
```
這是升級後:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate:status
database: D:/只要有心,人人都可以作卡米狗/ironman/db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20180110181744 Create keyword mappings
D:\只要有心,人人都可以作卡米狗\ironman>
```
資料庫已就緒,接下來就只等我們把學習紀錄寫入了。接下來我會試著用 [Google 試算表](https://www.google.com/intl/zh-TW_tw/sheets/about/)來比喻目前資料庫的狀態(因為我家沒有 Excel),現在的資料庫看起來像這樣:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEickLSuTINz3yHZ5q7CnPiF_T8L3ol03H_B9QD6aZTgXcgcRjTadqjjKvohbY7B9tL1wLPnDrDHwl6xMSeCJ9nBUO_1BLlp6-lDVJmFFgcLLXm7J0a0kePKxQPBKkRuOf55Ivb79LRI4qc/s1600/1.jpg)
# 資料模型
我們可以使用 `rails console` 或簡寫 `rails c` 去試著操作看看資料模型, `rails console` 是一個類似 `irb` 的互動式介面,他可以讓你輸入一行程式就立即生效。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails console
Loading development environment (Rails 5.1.4)
irb(main):001:0>
```
### 列出所有資料
我們的資料模型叫做 `KeywordMapping`。可以用 `.all` 將它顯示出來看看:
```
irb(main):001:0> KeywordMapping.all
KeywordMapping Load (2.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #
irb(main):002:0>
```
這一段:
```
SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ?
```
是我們對資料庫進行查詢的 SQL 語法,幸好你不需要學會這個,就當作沒看到吧。
而這一段:
```
=> #
```
是指 `KeywordMapping.all` 是一個 `ActiveRecord::Relation` 類別的實體, `[]` 表示它是空的。
讓我們弄點東西進去。
### 新增資料
用 `KeywordMapping.new` 可以獲得一筆新的空白資料。
```
irb(main):002:0> new_data = KeywordMapping.new
=> #
irb(main):003:0>
```
用一個變數 `new_data` 去接住它,因為我們接下來要對他做事。
```
irb(main):003:0> new_data.keyword = "Q"
=> "Q"
```
設定 new_data 的 keyword 是 "Q"。
```
irb(main):004:0> new_data.message = "A"
=> "A"
irb(main):005:0>
```
設定 new_data 的 message 是 "A"。
都設定好之後用 `new_data.save` 來存檔。
```
irb(main):005:0> new_data.save
(0.0ms) begin transaction
SQL (494.1ms) INSERT INTO "keyword_mappings" ("keyword", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["keyword", "Q"], ["message", "A"], ["created_at", "2018-01-11 14:30:55.567172"], ["updated_at", "2018-01-11 14:30:55.567172"]]
(52.5ms) commit transaction
=> true
irb(main):006:0>
```
一樣,就是一堆不需要看懂的 SQL。現在使用 `KeywordMapping.all` 就能看到新加入的資料了。
目前資料表的狀態:
```
irb(main):006:0> KeywordMapping.all
KeywordMapping Load (0.0ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #]>
irb(main):007:0>
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkE0jCHbZillsWuf2MYvDXyzLpJGKfqt6Ow49-QI2SjGA7FlpbcIaRR93ijS24lMv6VPzWfx_ft4pvSDlfRJxnls2W0PrwZxjY1-ri1kHGCgLn_2RoXXrOJDJAo6P0l7oldkNRdbxiaYw/s1600/2.jpg)
新增資料也有簡寫的方式可以一行做完,指令是 `KeywordMapping.create({keyword:"Q2", message:"A2"})`:
```
irb(main):007:0> KeywordMapping.create(keyword:"Q2", message:"A2")
(0.0ms) begin transaction
SQL (485.4ms) INSERT INTO "keyword_mappings" ("keyword", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["keyword", "Q2"], ["message", "A2"], ["created_at", "2018-01-11 14:36:34.858893"], ["updated_at", "2018-01-11 14:36:34.858893"]]
(48.5ms) commit transaction
=> #
irb(main):008:0>
```
這是傳入一個 hash 作為設定,用 `create` 方法的話就會自動 `save`,所以就不用自己再打 `save` 了。
目前資料表的狀態:
```
irb(main):009:0> KeywordMapping.all
KeywordMapping Load (0.0ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #, #]>
irb(main):010:0>
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaVO4FJfMRkqihoMfkBGqYh8EjY_lJhNh7gL_zR9UTh3aZbIyVu7G5y7AHrF7DStxx0R2w62vcR2szqN822SCYNe6v3UjUsBJRRpkzMoGmaRImwEwIb2wEsr2UH6zu-PUO08Ua-FeoH-A/s1600/3.jpg)
### 查詢資料
我們通常不會想要拿出整個資料表,而是只想要查當中的一筆,這時候就要用 `where` 方法,以下示範 `KeywordMapping.where(keyword:"Q2")`。
```
irb(main):008:0> KeywordMapping.where(keyword:"Q2")
KeywordMapping Load (0.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."keyword" = ? LIMIT ? [["keyword", "Q2"], ["LIMIT", 11]]
=> #]>
irb(main):009:0>
```
這是篩選功能,我們對 `keyword` 欄位做 `Q2` 篩選,在 Google 試算表按照順序點就可以達到相同的效果。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUURzgRjIKIolm51sGciqE7YjmnesYZBadibFRZS9v3lfm-DtqbNBD3RthlmOtSng6bNfNnx3Zf_ok5KWGBSXxG8VLlZ096Y6aUG54Vprsiw4pu4iDns-Sc4RSIqtpJCR6g6gWr1AB_-E/s1600/4.jpg)
篩選出來可能會有多筆,我們可以 `.all` 取得全部或者 `.first` 取第一筆,或 `.last` 取最後一筆。
所以卡米狗觸發教學的寫法是這樣:`KeywordMapping.where(keyword:"Q2").last.message`,對 `keyword` 欄位做篩選,找到最後一次教學紀錄,然後取出 `message` 欄位的內容。
```
irb(main):012:0* KeywordMapping.where(keyword:"Q2").last.message
KeywordMapping Load (0.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."keyword" = ? ORDER BY "keyword_mappings"."id" DESC LIMIT ? [["keyword", "Q2"], ["LIMIT", 1]]
=> "A2"
irb(main):013:0>
```
丟 Q2 進去資料庫查,查到 A2 再回應給 Line。
# 本日重點
- 學會使用資料庫遷移
- 學會使用資料模型
- 了解卡米狗觸發教學的原理
明天會講怎麼用這兩天學到的東西做出卡米狗學習指令。
第二十三天:認識資料庫
我們預計下一個要完成的功能是教學指令。
這是昨天的關鍵字回覆:
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
```
其中的學習紀錄表應該要能隨著大家講的話而去新增內容。這表示學習紀錄表應該要保存在檔案或資料庫內,每當需要存取學習紀錄表時就去存取檔案或資料庫。
今天講資料庫應該就飽了。
# 安裝 postgresql
查了一下發現是一條艱難的路,`postgresql` 在 Windows 上安裝的過程太過繁瑣,這裡就跳過不講,我們可以選擇在開發環境使用 `sqlite3`,同時在 Heroku 上使用 `postgresql`。當然你要挑戰在開發環境使用 `postgresql` 也行。這裡提供一個連結給你參考一下:[https://stackoverflow.com/questions/11656410/postgresql-installation-failed](https://stackoverflow.com/questions/11656410/postgresql-installation-failed),是不是令人看了就想崩潰呢~如果是 macOS 的話瞬間就裝完囉。
# 設定 database.yml
我們要修改的檔案位於 `config/database.yml`。
這是一開始的樣子:
```
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3
```
要改成這樣:
```
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
adapter: sqlite3
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
adapter: sqlite3
database: db/test.sqlite3
production:
<<: *default
database: ironman
```
先解釋一下,這裡有四段程式(其實不是程式,是設定檔)。
這是第一段:
```
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
```
這是作為一個預設值。
- adapter:要採用哪一套資料庫,預設使用 `postgresql`
- pool:是同時連線數量,可以理解成頻寬
- timeout:超過 5000 毫秒資料庫還不回應的話就當作逾時
```
development:
<<: *default
adapter: sqlite3
database: db/development.sqlite3
```
這段是開發環境,也就是我們的電腦。
- <<: *default:採用預設值
- adapter:要採用哪一套資料庫,這裡是使用 `sqlite3`,會把預設值覆蓋掉
- database:資料庫的儲存位置,`db/development.sqlite3` 這是一個路徑,你可以在專案資料夾裡面找到這個位置
```
test:
<<: *default
adapter: sqlite3
database: db/test.sqlite3
```
這段是測試環境,目前我們沒有使用測試環境,所以這裡跳過不講。
```
production:
<<: *default
database: ironman
```
這段是發布環境或正式環境,也就是 heroku 上。
- database:使用 `postgresql` 的話就不是儲存位置了,而是資料庫的名稱,不過概念上差不多。和 `sqlite3` 的差別是你不會在專案資料夾裡頭找到資料庫的實體檔案。
# 設定 Gemfile
因為我們要在 development 環境下使用 `sqlite3`,production 環境下使用 `postgresql`,所以 Gemfile 也要改寫,這是原本的 Gemfile:
```
gem 'pg', '~> 0.21.0'
```
要改寫為
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg', '~> 0.21.0'
end
```
意思就是在 development 和 test 環境下要使用 `sqlite3`,而在 production 環境下使用 `postgresql`。
# 建立資料庫
在小黑框輸入:
```
rails db:create
```
就可以得到一個空白的資料庫,你可以觀察它會出現在專案資料夾下的 `db` 資料夾下。
# 建立資料表
在小黑框輸入:
```
rails generate model keyword_mapping keyword message
```
會看到:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails generate model keyword_mapping keyword message
invoke active_record
create db/migrate/20180110181744_create_keyword_mappings.rb
create app/models/keyword_mapping.rb
invoke test_unit
create test/models/keyword_mapping_test.rb
create test/fixtures/keyword_mappings.yml
D:\只要有心,人人都可以作卡米狗\ironman>
```
表示有四個檔案被生成了,分別是:
- 資料庫遷移檔:`db/migrate/20180110181744_create_keyword_mappings.rb`
- 資料模型:`app/models/keyword_mapping.rb`
- 單元測試:`test/models/keyword_mapping_test.rb`
- 測試資料:`test/models/keyword_mapping_test.rb`
因為我們不寫自動測試,所以後面兩個就先略過。
# 資料庫遷移檔
一個資料庫會有多個資料表,一個資料表會有多個欄位。以通訊錄為例,欄位大概就是姓名、電話、地址、信箱等等。大概長這樣:[https://goo.gl/VMT3CR](https://goo.gl/VMT3CR)。
建立一個空的資料表需要定義出這個表格有哪些欄位,分別儲存什麼格式的資料。
假設你現在在人工建立表格,你可能會開啟一個 Excel 然後在第一列上面輸入各種標題,說明下面每個格子該填什麼。但是工程師最不喜歡手動做事了,自動化就是潮。所以我們寫一隻程式去幫我們建立資料庫裡的表格,這些程式碼被稱為資料庫遷移檔。
但是工程師連資料庫遷移檔也懶得寫,所以就寫了一行指令自動生成資料庫遷移檔,也就是你剛剛輸入的那個指令。
打開 `db/migrate/20180110181744_create_keyword_mappings.rb` 會看到:
```
class CreateKeywordMappings < ActiveRecord::Migration[5.1]
def change
create_table :keyword_mappings do |t|
t.string :keyword
t.string :message
t.timestamps
end
end
end
```
重點在這裡:
```
create_table :keyword_mappings do |t|
t.string :keyword
t.string :message
t.timestamps
```
建立一個資料表叫 `keyword_mappings`,資料表包含兩個欄位,分別是 `keyword` 和 `message`,都是存字串。
# 資料模型
打開 `app/models/keyword_mapping.rb` 會看到:
```
class KeywordMapping < ApplicationRecord
end
```
空的,因為它用繼承 (`<`),其實有很多東西是藏在 ApplicationRecord 裡面。
所以我們剛剛輸入的指令是這樣:
```
rails generate model keyword_mapping keyword message
```
意思是我要生成一個資料模型和資料庫遷移檔,資料表名稱為 `keyword_mapping`,包含兩個欄位分別是 `keyword` 和 `message`。
今天先講到這裡。
資料庫博大精深,卡米狗不是一天造成的,要有耐心。
2018/1/10
在 rails 上傳圖片並進行裁切時遭遇到的神奇問題
markdown
我使用 carrierwave 來做圖片上傳,並使用產生器來生成 uploader,像這樣:
```
rails g uploader normal
```
會生成這樣的檔案:
```
class NormalUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
```
若想要在上傳圖片時,對圖片進行裁切操作,可以這樣寫:
```
class CropUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
process :crop
def crop
manipulate! do |img|
x = 1
y = 2
w = 3
h = 4
img.crop("#{w}x#{h}+#{x}+#{y}")
end
end
end
```
其中
```
include CarrierWave::MiniMagick
```
需要 gem "mini_magick" 以及 brew install imagemagick。
```
process :crop
```
這是一個 callback,會在圖片儲存前給你一個機會對圖片做事。所以只要前端傳遞一個矩形座標到後端就能切圖。這裡就隨便用 4 個值意思一下。
```
img.crop("#{w}x#{h}+#{x}+#{y}")
```
這個 crop 方法會被轉為系統指令
```
mogrify -crop 3x4+1+2 file_path
```
mogrify 是 imagemagick 提供的指令,可以拿它來切圖。
說明書在這裡:[https://www.imagemagick.org/script/mogrify.php](https://www.imagemagick.org/script/mogrify.php)
一切運作良好,直到我遇到這張圖:[https://www.ncl.ucar.edu/Applications/Images/color_18_3_lg.png](https://www.ncl.ucar.edu/Applications/Images/color_18_3_lg.png)
怎麼切位置都是錯的。
[強者我同事](https://blog.frost.tw)爬了一下文,發現是 ImageMagick 支援叫做 Virtual Canvas (虛擬圖層?)的資訊,這種東西其實是圖片的 Metadata 的一部分。
把出問題那張圖片拿去解析 Metadata:[https://www.get-metadata.com/result/56d8b843-db53-4d7f-9f8a-7b1cd1ebde9b](https://www.get-metadata.com/result/56d8b843-db53-4d7f-9f8a-7b1cd1ebde9b)
會發現
```
Image Offset: 54, 64
```
也就是 ImageMagick 發現他有設定位移,所以就照這個設定去裁切了。然後用 +repage 可以讓他把 Offset 設回 0,0
所以這是我們的目標指令:
```
mogrify +repage -crop 3x4+1+2 file_path
```
但 ruby 是要這樣寫:
```
img.combine_options do |c|
c.repage.+
c.crop("#{w}x#{h}+#{x}+#{y}")
end
```
因為有兩個以上的參數,所以需要用 combine_options 去串接參數。
```
c.repage.+
```
會生成出
```
+repage
```
事實上他會把函數名稱拿去當作參數名稱,如果我這樣寫:
```
img.combine_options do |c|
c.jsdiofaodj.+
c.crop("#{w}x#{h}+#{x}+#{y}")
end
```
他就會嘗試執行
```
mogrify +jsdiofaodj -crop 199x154+234+343 file_path
```
如果把 .+ 拔掉:
```
img.combine_options do |c|
c.jsdiofaodj
c.crop("#{w}x#{h}+#{x}+#{y}")
end
```
就會變成
```
mogrify -jsdiofaodj -crop 199x154+234+343 file_path
```
如果調換順序:
```
img.combine_options do |c|
c.crop("#{w}x#{h}+#{x}+#{y}")
c.repage.+
end
```
會變成
```
mogrify -crop 3x4+1+2 +repage file_path
```
你可能會想說,參數順序有差嗎?還真的有差。
因為他不是參數順序,而是執行順序。
總而言之,[強者我同事](https://blog.frost.tw)守護了世界的和平。
參考資料:
[https://github.com/minimagick/minimagick/issues/107](https://github.com/minimagick/minimagick/issues/107)
第二十二天:用 Line Messaging API 實作關鍵字回覆
markdown
今天我要讓你能抓到一點寫程式的感覺,所以我們會一直不斷地修改程式碼,這麼做可以讓你對程式碼的操作更熟悉。
先從最簡單的功能開始作,今天的目標是讓卡米狗能針對關鍵字回應訊息。
# 程式碼的重構
在加程式碼之前,我們先整理一下目前的程式
```
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
```
目前程式碼是這樣,我覺得有點太長了,我們要讓他更好閱讀。首先要制定一個目標。
```
def webhook
# 核心程式
reply_message = reply(received_message)
# 回覆訊息
reply_to_line(message)
# 回應 200
head :ok
end
```
只保留最重要的,當卡米狗看到 `received_message` 時,要回應 `reply_message`,剩下的東西都放到別處。我們先假設卡米狗只會對純文字有反應,並且只會回應純文字。
# 移出 client
```
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
```
我們把這段程式搬出去,變成這樣:
```
# Line Bot API 物件初始化
def line
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
定義一個方法叫作 `line`,他會回傳一個 client。這裡可以省略區域變數 client 不寫。
```
# Line Bot API 物件初始化
def 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
```
這麼寫的話,每次呼叫 `line` 時,就都會去作一次 `Line::Bot::Client.new`。我們可以把它保存起來,第二次呼叫 `line` 的時候就把保存起來的部分拿出來用,這樣作可以增加效能。
```
# Line Bot API 物件初始化
def line
return @line unless @line.nil?
@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
```
如果 `@line` 有值的話,直接回傳 `@line`,沒有值的話才作 `Line::Bot::Client.new` 並保存到 `@line`。
這裡用到了 `@`,`@` 開頭的變數是實體變數,跟區域變數不同的是,實體變數的記憶比較持久,區域變數只要函數執行完就消失,但實體變數可以持續存活到第二次之後的函數執行,甚至我可以在A函數保存實體變數,在B函數去使用實體變數。
關於實體變數,詳細的教學請參考:[為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈](https://railsbook.tw/chapters/05-ruby-basic-1.html#variable-and-constant)
現在的程式已經足夠完美了,但有更精簡的寫法。
```
# 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
```
`||` 是或的意思,這是一個很特殊的寫法,跟原本的程式碼效果幾乎相同,我就不多作解釋。沒學會 `||=` 也沒關係,這就是工程師der浪漫。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '移出 client'
}
# 傳送訊息
response = line.reply_message(reply_token, message)
# 回應 200
head :ok
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
...下略
end
```
改到這邊可以上傳程式碼測試一下,如果你不測也沒關係,因為我們還要繼續改。
# 移出 reply token
我們作一個函數,讓他自己去抓 reply token,我們關心的是要發什麼話,不關心 reply_token,所以我希望我們的主程式能變成這樣。
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
```
所以我們要實作函數 `reply_to_line`,他是一個傳入 message 後,透過 line api 傳送訊息出去,並傳回 HTTP response 的函數。
```
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
response = line.reply_message(reply_token, message)
end
```
可以再精簡為:
```
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
```
因為一個函數的傳回值是最後一行的執行結果。這就是工程師der浪漫。
其實我們大部分時間在作的事情都是搬移程式,其實寫程式就是一種整理的藝術。
你可以想像成我們東西一開始全都放在客廳。東西越來越多之後,客廳就會開始變亂。要整理客廳的方法就是買幾個櫃子後把東西放進櫃子。函數就是我們的櫃子。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
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
...下略
end
```
# 移出 message
這是現在的主程式:
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
```
我不希望在主程式看到這些:
```
{
type: 'text',
text: 'ㄅㄌㄅㄌㄅㄌ'
}
```
因為我們只在乎 `'ㄅㄌㄅㄌㄅㄌ'` 的部分,所以剩餘的部分都要盡量外移。這就像你不會想把垃圾放在桌上一樣,找個垃圾桶放垃圾就對了。
設定目標:
```
def webhook
# 設定回覆訊息
reply_text = '移出 message'
# 傳送訊息
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
```
這就是我們所希望的樣子,因此我們要修改 `reply_to_line` 這個函數。
```
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
其實就是把一開始的程式整個全搬到 `reply_to_line`。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = '移出 message'
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 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
...下略
end
```
現在的主程式已經比一開始乾淨許多,這時我們再來加功能,應該就會比較容易看出我們在加什麼功能。我們要加的功能是關鍵字回覆。
# 關鍵字回覆
我們希望主程式可以變成這樣:
```
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
```
所以我們需要兩個函數,一個是 `received_text`:傳回對方說的話,另一個函數是 `keyword_reply`:傳入對方說的話,傳回卡米狗應該說的話。
這種思考邏輯是先決定程式的大架構,再來描述細節。這就像在畫圖的時候,你會先打個草稿,草稿看起來OK了再去畫細節,這樣可以確保你不會太專注於細節而失去了整體比例。
```
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
```
現在這樣就表示你說什麼,卡米狗就會跟著說什麼。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
...下略
end
```
這程式碼可以運作,你可以上傳程式碼玩玩看。
這兩個函數現在這樣都還算未完成,他們都有一些缺陷。
先講關鍵字回覆。這裡應該要作成看見A回答B,而不是看見A回答A,我們還缺一個學習紀錄表讓卡米狗查。
而 `received_text` 的問題是,如果 Line 傳來的通知並不是訊息通知,而是比方說有人加你好友,或邀請你進入群組,或傳送貼圖、圖片、聲音、檔案都可能會導致我們的程式直接掛掉,因為 `params['events'][0]['message']['text']` 的值是空值,而我們沒有說當是空值的時候應該怎麼作。
# 處理 received_text 的空值問題
根據 Line Messaging API 的文件,當有人傳訊息來時,我們才會收到 `message` 這個 hash,也就是說,下面這行不一定有值。
```
params['events'][0]['message']
```
我們應該在他有值的時候才去取 ['text']
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
nil
else
message['text']
end
end
```
這是我們第一次用到了 if ,當 `message.nil?` 是空值的時候,我們就會傳回 `nil`。不是空值的話,就傳回 `message['text']`。
加入一點工程師的浪漫就會變成這樣:
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
```
有的時候不用太堅持什麼浪漫,因為這純粹只是工程師的自爽行為。
# 關鍵字回覆的學習紀錄表
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
```
在這裡定義了學習紀錄表 `keyword_mapping` 之後就作查表。當查表查不到內容的時候,就會傳回 nil。
這表示當查不到內容時,卡米狗應該要不回應,這需要修改其他函數。
```
# 傳送訊息到 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
```
這裡加一行 `return nil if reply_text.nil?`,當傳入值為空時表示不回應,後面的程式碼就不用作了。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
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
```
用起來的效果是這樣:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFvB2Z-FMMJHkST4KTa555Wus0qw2BxKG3gi_710LZiHKWiftzM6ABpty-50xdVSqE_yzcr9cTSsRV685sim-q3FYXN0N0bOJPDi3O7F_AI05GGJKKusSEGV2SS5Xs0Vh0BynxavFRNCg/s1600/1.jpg)
那個「讓我想想...」是我們在[第三天:作一隻最簡單的 Line 聊天機器人](https://ithelp.ithome.com.tw/articles/10192928)從 Line 後台作的設定,如果你不喜歡可以去後台把它關掉,它在這裡:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDVJWISLeewPF0mQvK0ocHoSmWUIhyphenhyphentsvrm_nSoK0v1F6aaV6NeoXn_S67y2lJ_fSCN78wwKRYC2xKXss15CtU_vgnD7pobZgbwMoN149oncnoq92YsNx9Q-76IIGr4U9jQf68wkvMGAs/s1600/2.jpg)
今天就講到這,明天講怎麼教卡米狗說話。
2018/1/9
第二十一天:讓 Line Bot 回覆訊息
markdown
昨天我們把聊天機器人 webhook 串好了,今天我們要讓機器人回覆訊息。
我們先來看看我們能不能正常的收到訂閱通知,我說的訂閱通知是在[第五天:認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441) 介紹到的各種通知。
我們可以看在 heroku 上的 `rails server` 小黑框來觀察。
# 觀察 heroku 上的伺服器運作記錄
當我們在小黑框輸入 `heroku logs -t` 時,跟我們在小黑框輸入 `rails server` 有 87% 像,當有人開啟網頁時,就會看到運作紀錄(log)。
```
D:\只要有心,人人都可以作卡米狗\ironman>heroku logs -t
```
先開個小黑框輸入 `heroku logs -t` 之後放著。
# 觀察 Line developer 後台的 verify
我們先回到 Line developer 後台,點一下昨天點過的 `verify`,看看 Line 到底傳了什麼給我們的伺服器。
```
2018-01-08T15:04:43.091880+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 21213 -e production`
2018-01-08T15:04:50.486685+00:00 app[web.1]: => Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: => Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: => Run `rails server -h` for more startup options
2018-01-08T15:04:50.486719+00:00 app[web.1]: Puma starting in single mode...
2018-01-08T15:04:50.486725+00:00 app[web.1]: * Version 3.11.0 (ruby 2.3.4-p301), codename: Love Song
2018-01-08T15:04:50.486733+00:00 app[web.1]: * Min threads: 5, max threads: 5
2018-01-08T15:04:50.486735+00:00 app[web.1]: * Environment: production
2018-01-08T15:04:50.486886+00:00 app[web.1]: * Listening on tcp://0.0.0.0:21213
2018-01-08T15:04:50.487396+00:00 app[web.1]: Use Ctrl-C to stop
2018-01-08T15:04:38.156226+00:00 heroku[web.1]: Unidling
2018-01-08T15:04:38.156470+00:00 heroku[web.1]: State changed from down to starting
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Started POST "/kamigo/webhook" for 203.104.156.74 at 2018-01-08 15:04:52 +0000
2018-01-08T15:04:52.276728+00:00 app[web.1]: I, [2018-01-08T15:04:52.276573 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Processing by KamigoController#webhook as HTML
2018-01-08T15:04:52.277008+00:00 app[web.1]: I, [2018-01-08T15:04:52.276880 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Parameters: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
2018-01-08T15:04:52.297993+00:00 app[web.1]: W, [2018-01-08T15:04:52.297845 #4] WARN -- : [34361d41-4be8-4293-971d-eca0151ea11c] Can't verify CSRF token authenticity.
2018-01-08T15:04:52.298635+00:00 app[web.1]: I, [2018-01-08T15:04:52.298565 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Completed 200 OK in 21ms
2018-01-08T15:04:52.306974+00:00 heroku[router]: at=info method=POST path="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=34361d41-4be8-4293-971d-eca0151ea11c fwd="203.104.156.74" dyno=web.1 connect=0ms service=56ms status=200 bytes=289 protocol=https
```
我們需要看懂這些東西,我們一次讀一段,慢慢讀完。
第一行:
```
2018-01-08T15:04:43.091880+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 21213 -e production`
```
這行就是我們平常在小黑框輸入的 rails server,heroku 上也需要輸入這行,但這是由 heroku 自動幫我們輸入。
heroku 的免費伺服器有一個缺點,如果連續 30 分鐘都沒人連到我們的網頁伺服器,他就會自動關機。當他處於關機狀態時,如果有人連到我們的伺服器,那他就會把我們的網頁伺服器叫醒。
```
2018-01-08T15:04:50.486685+00:00 app[web.1]: => Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: => Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: => Run `rails server -h` for more startup options
2018-01-08T15:04:50.486719+00:00 app[web.1]: Puma starting in single mode...
2018-01-08T15:04:50.486725+00:00 app[web.1]: * Version 3.11.0 (ruby 2.3.4-p301), codename: Love Song
2018-01-08T15:04:50.486733+00:00 app[web.1]: * Min threads: 5, max threads: 5
2018-01-08T15:04:50.486735+00:00 app[web.1]: * Environment: production
2018-01-08T15:04:50.486886+00:00 app[web.1]: * Listening on tcp://0.0.0.0:21213
2018-01-08T15:04:50.487396+00:00 app[web.1]: Use Ctrl-C to stop
```
這是我們平常下 `rails server` 指令時會看到的訊息。
```
2018-01-08T15:04:38.156226+00:00 heroku[web.1]: Unidling
2018-01-08T15:04:38.156470+00:00 heroku[web.1]: State changed from down to starting
```
heroku 的狀態從關機變成開始運作中。
```
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Started POST "/kamigo/webhook" for 203.104.156.74 at 2018-01-08 15:04:52 +0000
2018-01-08T15:04:52.276728+00:00 app[web.1]: I, [2018-01-08T15:04:52.276573 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Processing by KamigoController#webhook as HTML
2018-01-08T15:04:52.277008+00:00 app[web.1]: I, [2018-01-08T15:04:52.276880 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Parameters: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
2018-01-08T15:04:52.297993+00:00 app[web.1]: W, [2018-01-08T15:04:52.297845 #4] WARN -- : [34361d41-4be8-4293-971d-eca0151ea11c] Can't verify CSRF token authenticity.
2018-01-08T15:04:52.298635+00:00 app[web.1]: I, [2018-01-08T15:04:52.298565 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Completed 200 OK in 21ms
```
每一行的開頭都有這個:
```
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4] INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c]
```
這不是很重要,因為會妨礙閱讀,所以我們先忽略他,以下是忽略後的結果:
```
Started POST "/kamigo/webhook" for 203.104.156.74 at 2018-01-08 15:04:52 +0000
Processing by KamigoController#webhook as HTML
Parameters: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
Can't verify CSRF token authenticity.
Completed 200 OK in 21ms
```
這看起來就有點眼熟了。
他打了一個 POST 到我們的 `/kamigo/webhook`,並且傳了這樣的參數:
```
{
"events"=>[
{
"replyToken"=>"00000000000000000000000000000000",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
},
{
"replyToken"=>"ffffffffffffffffffffffffffffffff",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
}
],
"kamigo"=>{
"events"=>[
{
"replyToken"=>"00000000000000000000000000000000",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
},
{
"replyToken"=>"ffffffffffffffffffffffffffffffff",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
}
]
}
}
```
### 解讀 POST BODY
經過我精美的排版之後變得比較好閱讀了,這是一個很大的 Ruby hash(雜湊陣列),我們在[第十三天:認識 Ruby 的資料型態](https://ithelp.ithome.com.tw/articles/10195196)學過這個。我把這個 hash 存成檔案,名叫 `line_verify.rb`,這麼作有一些好處,sublime text 會幫文字加顏色,以及可以使用縮小/展開功能。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdkzsNhAA2cBJPDvvPOPsB4K1HgCXaN8xaH2P9sFo00qRPPMS9B2q8cfghdUAifmfPEsJzwOEm5bHU5B_WByr2IOIYhTJnMe5vE_d6hFpe3lkMEYtRvnasucbufklhYlBzNFDHm4JdgSo/s1600/1.jpg)
這個 hash 有兩個 key,分別是 events 和 kamigo,events 是一個陣列,而 kamigo 是一個 hash。
kamigo 是一個只有一個 key 的 hash,而這個 key 也叫作,events。更巧的是,這兩個 events 裡面包含的資料是相同的,所以我們只要看其中一個 events 就好。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0vgYz9C60_y5_uWDOuAXchjDkJmNPKL3XOVBzJnq9tqwkoalA8cZWdWrVItyAD-l6fBoz7Z-gtdcyKnhRW638QzLHTioVILU20nya6jjCBVmkErg-ui71mW3FvY-d970O_7qzRiKIr7U/s1600/2.jpg)
這個 events 陣列,包含兩個 hash。這個 hash 我們在[第五天:認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441) 時已經介紹過了。這裡再簡單複習一下:
- replyToken:是我們要回覆訊息時必須傳回的值,他代表收件者以及你的回覆權。
- type:通知類型,這是一則訊息
- timestamp:傳送時間,我們通常會忽略他
- source:發信者
- message:訊息內容
接下來看第一個 hash 裡的 source 和 message。
```
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
}
```
這是發生在私訊對話框,對方的 id 是 Udeadbeefdeadbeefdeadbeefdeadbeef。
```
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
```
對方傳來的訊息是文字訊息:「Hello, world」。
再看第二個 hash。第二個 hash 裡的 source 跟第一個一樣,所以我們只需要看 message 的部分。
```
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
```
對方傳來的訊息是貼圖訊息。
# 觀察實際傳遞的訊息
讓我們試著用 Line 傳訊息給我們的聊天機器人,看看會收到什麼:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMlyQ7a8tL5J9LNC5HKqeEYEnxbFx5GWsV6HeyZO6C0kveLtZapIP1pRPTTvqW_-qcj2fhWFSpDgW3wItgqCrAolvQXvWk8Ye8OVUWMrn5bYMLjECXjplsSffUWoxTBPijw9fv_MyjJjA/s1600/4.jpg)
看看小黑框,什麼事也沒發生。原來是 Line developer 後台還有東西沒設定。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYMwu_rXKeZdke_vrCzWpmCi7-DFWSincuXup6uP7XSiiQeP4Y_ZVZOY8_81DP-4nvpzYhD_T1bUGSc92HLhpU4IBYHgF0g44UGh7or7NxdhSwuYGnIhBbzSU-TX8ok8JoZYDUWdAl44g/s1600/3.jpg)
這兩個分別是`使用 Webhook` 和`可被邀請進群`,都把他打開。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-PXDkSv3QyTPe3QCGxuJhe3lB2PUG-pbPYfeVHGEwBT-FBuc3Klr4wrLTBCLFW5UtcDIEiYFVvzMReclB3OnCay4nEd3n7E41kncvsvNSPqA1sKYq-8lIngKUe36E5-NThbfCJEbTNBU/s1600/5.jpg)
都開啟後的樣子:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj19dYqISo4Kdla-qB0i1RpuBfyBxz-HwkE5niTLpK0KVNZRTYjU0qZHZG6Hd1g78GFV4BANHoHl_ZoN5srguHahxElSAH88hv0-A9MhSnyGTQ5mAo6dJW5ZcnTWc77S_mLf3_6d7zy-zs/s1600/6.jpg)
都開啟後再次傳訊息就會在小黑框看到:
```
{
"events"=>[
{
"type"=>"message",
"replyToken"=>"1d0bc2113fd344deb8131750e8d2daa2",
"source"=>{
"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a",
"type"=>"user"
},
"timestamp"=>1515432401148,
"message"=>{
"type"=>"text",
"id"=>"7279127481400",
"text"=>"五五六六得第一"
}
}
]
}
```
我把不重要的部分都刪除了,剩下來的部分看起來差不多,應該不用多作解釋,現在我們來作回覆訊息。
# 回覆訊息
### 安裝 Line Bot API
我們要先安裝一個 Line 提供的 gem,在 Gemfile 加入以下程式:
```
# line
gem 'line-bot-api'
```
記得要在小黑框輸入 bundle 下載套件。
### 引入 Line Bot API
修改 `app/controllers/kamigo_controller.rb` 檔,在第一行加上`require 'line/bot'`:
```
require 'line/bot'
```
這表示我們要使用 Line 的套件。
### 修改 webhook
這個是 webhook 寫好的樣子,我會慢慢解釋他。
```
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
```
### Line Bot API 物件初始化
```
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
```
這段程式是我們使用 Line Bot API 內提供的物件,需要傳入兩個字串分別為 `Channel secret` 和 `Channel access token`。
你可以在 Line Developer 後台找到他們:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMvtwmgO89PEdgHdB0V2NHW9qlg0zUcfvXdFIS1ylTW3DYO8Qiu7k5mxDNaYhElJ2kNl_A2b_chXFI3_ycGCIHa752GQRWxutaWFfJnsnJiE8F53lcYxWblotLJ9O7U-3slyNteUNhPLk/s1600/7.jpg)
只要按下 `Issue` 按鈕,值就會變。
### 取得 reply token
```
reply_token = params['events'][0]['replyToken']
```
根據剛剛我們觀察的 hash 結構,我們大膽假設 `events` 陣列裡只有一筆資料,用 `[0]` 取得第一筆,然後取得他裡面的 `replyToken`。
### 設定回覆訊息
```
message = {
type: 'text',
text: '好哦~好哦~'
}
```
我們回覆的訊息是純文字訊息,內容是「好哦~好哦~」
### 傳送訊息
```
response = client.reply_message(reply_token, message)
```
我們透過 Line Bot API 包裝好的函數來回覆訊息,這樣我們就不用去寫在[第十六天:做一個最簡單的爬蟲](https://ithelp.ithome.com.tw/articles/10195760)寫過的那些東西。
### 對一下程式碼
這是完整的 `app/controllers/kamigo_controller.rb` 檔內容:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
...下略
end
```
### 上傳程式碼
確認沒問題之後,因為我們沒辦法進行本地端的測試,所以就直接上傳程式碼:
```
git add .
git commit -m "回覆訊息"
git push heroku master
```
上傳完成後進行測試。
### 實測
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW5PWuoQ-kxXMRNjvTQt0Rvfzq-krPNr9UOVO5L0ny5rKfZEjALXF_VHVJ1blCn3sx2AOIGRrgKvi05O9Vh3hTYxRnqnMa01nhcxSOgwK085R2yTVY0miyy-ryT9J36mqp0lkLdSrX_DI/s1600/8.jpg)
耶~我們終於作出最簡單的回應了。
如果你沒有辦法順利抵達這裡,那麼你有幾個可以作的事情,你可以讓 `heroku logs -t` 打出你想知道的內容。比方說,你想知道你的 reply_token 有沒有抓對:
```
# 取得 reply token
reply_token = params['events'][0]['replyToken']
p "======這裡是 reply_token ======"
p reply_token
p "============"
```
像這樣加上幾行 `p`,接著只要對 Line Bot 傳訊息,你就能在小黑框看到這個:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkY2KcbiPvHLBz61ktvryWb6JL00hq45xDqHi6JCAkhF4zYbE2AhdnbKQhyRk2mRhHA9t4-AuDK68TQct562NB5rRwBgbsooyTV3Ru7xlgVxDZ8XU33_u4PE6shXX1L9sr5VpMlMfdD30/s1600/9.jpg)
你可以把所有的變數都用 `p` 印出來慢慢看,看是不是你想要的值。其中最值得觀察的變數是 `response`。
如果你看不懂,可以對 logs 截圖在底下留言。
# 總結
- 你知道不同的通知只有些許差異。
- 你看懂通知傳遞的內容,以及如何用程式碼擷取出他的值
- 你學會了使用套件
- 你學會了怎麼讓 Line Bot 回覆訊息
- 你學會了在 heroku 上的除錯方法
明天之後就會越來越難了,要寫的程式會越來越多。這裡有一些 Ruby 的基礎教學,有興趣的人可以補一下:
[為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈](https://railsbook.tw/chapters/05-ruby-basic-1.html)
[為你自己學 Ruby on Rails - 數字、字串、陣列、範圍、雜湊、符號](https://railsbook.tw/chapters/06-ruby-basic-2.html)
[為你自己學 Ruby on Rails - 方法與程式碼區塊(block)](https://railsbook.tw/chapters/07-ruby-basic-3.html)
[為你自己學 Ruby on Rails - 類別(Class)與模組(Module)](https://railsbook.tw/chapters/08-ruby-basic-4.html)
訂閱:
文章 (Atom)