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://3.bp.blogspot.com/-fl8HAcLjuWc/Wl9pvWWdUOI/AAAAAAABAi0/9_2nRgw_mTk2FT5C8B1uznYQ5EfnTQ4twCLcBGAs/s1600/1.jpg) Line app 指的是手機或PC版的 Line,Line server 在收到訊息後會透過 webhook url 傳遞給我們。接著我們會打 `line.reply_message` 傳訊息給 Line server,最後再由 Line server 傳給 Line app (最後這段可能不是 HTTP request)。 # 發公告 ![](https://4.bp.blogspot.com/-Iiv5GQkRg7g/Wl9p_KXuBzI/AAAAAAABAi4/jTj7l4aFrpkn-NAaf78XxYzY247D_3cfQCLcBGAs/s1600/2.jpg) 我們透過後台管理介面填入公告訊息,用 `line.push_message` 傳訊息給 Line server。 # 排程公告 ![](https://2.bp.blogspot.com/-xTaEgNIKTPs/Wl-EmTezlaI/AAAAAAABAj0/LxPMWrAZz8wDHGFTe-RmexlmIdtJMFcxACLcBGAs/s1600/3.jpg) 有觀眾說想知道鬧鐘怎麼作,這裡再說明一下。 我們會用到 worker 來處理工作排程。首先是先在後台設定預約發訊息,然後將訊息儲存到工作清單,每個工作可以指定執行時間,接著就等時間到,worker 就會用 `line.push_message` 去打 Line server。 # 查天氣 ![](https://2.bp.blogspot.com/-Qfjvw0ORSqs/Wl9qQWo42II/AAAAAAABAjE/OTct_48STto9-xtbeLT4H9kUpP3FHyITwCLcBGAs/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://1.bp.blogspot.com/-I0LDIsNZ_OY/Wl-aUxMwQrI/AAAAAAABAkQ/cG5f06RPgqQgTu63O4fwUflg6ve87V6ygCLcBGAs/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://1.bp.blogspot.com/-sBciUMUVjjs/Wl-V69m3ZEI/AAAAAAABAkE/Nb_aiVNc7tI6Fbq_LnFNwj1mLyM6vNNzACLcBGAs/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://4.bp.blogspot.com/-d-WSKiru_3U/Wl-eQ6nLw1I/AAAAAAABAkc/R0M5wNGlwI0JlRYVB3a9nb2lQsuCDWZMgCLcBGAs/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://4.bp.blogspot.com/-N7C6oTly7og/WlzEzH6TdkI/AAAAAAABAho/B284RrW0ZE0wGEfSARpax3VxaFQWrQIFgCLcBGAs/s1600/1.jpg) ### new 新增資料頁 ![](https://1.bp.blogspot.com/-4hqCXwFoc9I/WlzEzdZjraI/AAAAAAABAhs/HnU0U7xxCskxBfXtvbszpFSorHzxDRmPgCLcBGAs/s1600/2.jpg) 隨便亂填: ![](https://2.bp.blogspot.com/-thqbsE4jUrU/WlzEzRsn7XI/AAAAAAABAhw/IIkutH7KhdgqheNs1D1hoe2bz5Ptgk4VACLcBGAs/s1600/3.jpg) ### show 檢視資料頁 ![](https://4.bp.blogspot.com/-lGAN5JvShcY/WlzE0R5gWeI/AAAAAAABAh0/SJZpP_JmgX0cFi_RUpmMQTRdwYw1Bd3XACLcBGAs/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:

<%= 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 => # 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 個字而已。 明天講怎麼發公告。