Edited at

【Otemachi.rb#10】Rails×Messaging API × Herokuで清宮幸太郎の成績検索BOTを作成したよ


はじめに

Railsの勉強のため、Line-Botを作ってみました。

清宮幸太郎の成績をほぼ毎日、帰宅中電車の中で検索しているので、清宮幸太郎の成績検索Botに決めました。

何とか動くようにはなったものの、中身を完全に理解していないため調べてみました。

今回作成したLine-Botは下記のGitリンクから見れるようになっています。

https://github.com/Keisukegdk/kiyomiya_line_bot

コントローラーの部分だけ冒頭に貼っておきます。


kiyomiya_bot_controller.rb

class KiyomiyaBotController < ApplicationController

require 'line/bot'
require 'mechanize'

protect_from_forgery :except => [:first]

def client
@client ||= Line::Bot::Client.new { |config|
config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
}
end

def first
body = request.body.read
signature = request.env['HTTP_X_LINE_SIGNATURE']
unless client.validate_signature(body, signature)
error 400 do 'Bad Request' end
end

def get_record #Yahooのサイトから清宮幸太郎のデータを取得して呼び出し元に返す
agent = Mechanize.new
page = agent.get("https://baseball.yahoo.co.jp/npb/player/1700025/")
average = page.search('.yjM').children[1].inner_text #打率の取得
homerun = page.search('.yjM').children[8].inner_text #本塁打数の取得
rbi = page.search('.yjM').children[8].inner_text #本塁打数の取得
records = {average: average, homerun: homerun, rbi: rbi}
return records
end

text_params = params["events"][0]["message"]["text"] #メッセージイベントからテキストの取得

events = client.parse_events_from(body)
events.each { |event|
case event
when Line::Bot::Event::Message
case event.type
when Line::Bot::Event::MessageType::Text
if text_params == "打点" then
message = {
type: 'text',
text: "#{get_record[:rbi]}打点です。"
}
client.reply_message(event['replyToken'], message)

elsif text_params == "打率" then
message = {
type: 'text',
text: "#{get_record[:average]}です。"
}
client.reply_message(event['replyToken'], message)

elsif text_params == "本塁打" then
message = {
type: 'text',
text: "#{get_record[:homerun]}本塁打です。"
}
client.reply_message(event['replyToken'], message)

else
message = {
type: 'text',
text: "「打率」、「打点」、「本塁打」のどれかを入力してください。"
}
client.reply_message(event['replyToken'], message)
end
end
end
}
head :ok
end
end



事前準備


「Messaging APIを利用するには」で確認すること

https://developers.line.me/ja/docs/messaging-api/getting-started/

1、コンソールへログインする

2、開発者として登録する

3、新規プロバイダーを作成する

4、チャネルを作成する

5、確認する


「ボットを作成する」で確認すること。

https://developers.line.me/ja/docs/messaging-api/building-bot/

1、始める前に

 ・ボット用のチャネルが作成されていこと

 ・Botをホストするサーバーを用意すること。(Herokuとか)

2、コンソールでボットを設定する

 ・APIを呼び出すためのチャネルアクセストークンが必要

 ・Webhookペイロードを受け取るためのWebhook URLが必要

3、チャネルアクセストークンを発行する

4、Webhook URLを設定する

5、ボットを友達登録する

私はこちらのサイトも参考にしました。

Herokuへの環境変数の設定や、Deploy方法が書かれています。

今更ながらRails5+line-bot-sdk-ruby+HerokuでLineBot作成してみたら、色々詰まったのでまとめました。


Line-Botで実装したこと

Lineメッセージに「本塁打」、「打点」、「打率」のどれかを入力したら、その入力情報に従ってスクレイピングしたデータを返すというもの。


完成までのアプローチ(記事の補足)

完成までの大まかなアプローチとして下記のように進んでいきました。

①Gitに貼られているおうむ返しBotをそのまま言われるがままに作る

②オウム返しBotのソースを見ながら、機能を追加・変更する

  【追加点】追加する機能は、Yahooサイトをスクレイプして成績を取得する

  【変更点】オウム返しではなく、メッセージイベント内のテキストに応じた処理を実行する


困った点

■そもそも書かれている構文とかがわからない

■「署名を検証する」で何を行っているかよくわからなかった

■LINEに投稿しているメッセージのテキストはどうやって取り出すのか


■そもそも書かれている構文とかがわからない

分らなかった部分を下記に1つづつ書き綴る


【1個目】||=・・・(Double Pipe / Or Equals)


kiyomiya_bot_controller.rb

def client

@client ||= Line::Bot::Client.new { |config|
config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
}
end

左辺偽または未定義ならば右辺の値を代入する。このイディオムは変数の初期化で良く用いられる。例えば、ログインしているユーザーかどうかを判定する。

a ||= b



a = a || b

と同じ

ここ参考にしました。

Ruby演算子 a ||= 1 の意味 (or イコール)

[2] pry(main)> a ||= 1

=> 1
[3] pry(main)> a = 5
=> 5
[4] pry(main)> a ||= 1
=> 5

確かに、aの値がある場合は1が代入され、aの値がある時(5がセットされている時)は1はセットできていない。

また他のサイト等も確認しているとcurrent_userメソッドなどを定義する時にRubyの自己代入をよく使うとのこと。・・・確かに見るしチュートリアルにも記載がありました。

Ruby on Rails Tutorial コラム8.1 「||=」とは何なのか


aaa.rb

if @current_user.nil?

@current_user = User.find_by(id: session[:user_id])
else
@current_user
end

これを一行で書くとこうなる


aaa.rb

@current_user = @current_user || User.find_by(id: session[:user_id])


さらに短縮するとこうなる


aaa.rb

@current_user ||= User.find_by(id: session[:user_id])


そして、、これが「王道だ!」という記載がGuideにも書かれているので今度から絶対使います。(怖いので)もっと実践だとPrams[:id]みたいな感じでセットするのでしょうか、、?


【2個目】Line::Bot::Client1・・・コロンコロン(モジュールの名前空間アクセス)およびそもそものモジュールの使い方?

そもそも、Moduleとは何だろう、classとメソッドと何が違うのだろうか、、使い分けは?

未だに把握しきれてないものの、調べてみよう。。

このあたりを参照してみた

モジュールの定義

Rubyのモジュールまとめ

ころんころんの話をしよう。もしくはモジュールの名前空間について。

■モジュールについて

・モジュールはクラスによく似た構成を持っている

・モジュールはメソッドを定義する事が出来る 

・クラス変数に相当するものはモジュールにはない

・classとの違いはクラスはクラスからオブジェクトを作成することができるがモジュールでは作成することは出来ない

・継承できない

・「モジュール名.メソッド名」の形式で関数のように実行する

・クラス内にインクルードして利用することが出来る

・名前空間として利用する

→モジュールを利用することによってクラスやメソッド名の重複を避けたり、他のプログラミング言語のように多重継承の実現、そしてクラスの肥大化を抑えれるんです!

::「ころんころん」について

::はスコープ演算子である

・モジュールやクラス内で定義された定数名を外部から参照するときによく利用する。

なるほど、、、、

今回参考にしたおうむ返しのコードも元からこんな感じでうわぁ~ってなっている


kiyomiya_bot_contoroller.rb

 events = client.parse_events_from(body)

events.each { |event|
case event
when Line::Bot::Event::Message
case event.type
when Line::Bot::Event::MessageType::Text
if text_params == "打点" then

なんでここまでModuleをここまでわけているのか、、

gitにどうなっているのか覗きに行ってみました。

line-bot-sdk-ruby/lib/line/bot/event/message.rb

当然ですが、、MessageType moduleとして細かく分けられていました。


lib/line/bot/event/message.rb


module Line
module Bot
module Event
module MessageType
Text = 'text'
Image = 'image'
Video = 'video'
Audio = 'audio'
File = 'file'
Location = 'location'
Sticker = 'sticker'
Unsupport = 'unsupport'
end
~省略~

APIレファレンスにもありますね。

image.png

Messaging-APIにおけるMuduleとは、メッセージタイプのことのようで、これらのModule配下の固有のidやtextイベントでいうのtextなどをその他と明確に区分けして利用できるようにするために使っているように見える。


【3個目】request.body.readってなんだ?

語感的に何をしているかは想像できるものの、具体的に何をしているかはわからないな〜ということで調べることに。

まず、requestに関しては、Railsのrequestメソッドだと思います。

アクセスしたユーザの情報を取得(request)

説明には「リクエストを送ってきたユーザのヘッダー情報や環境変数を取得する」とありますが、今回はヘッダーではなく、Bodyを取得しているようです。

ということでBodyとおまけでヘッダーを出力してみました。


ボディの出力結果.


body = request.body.read ← これ

"{\"events\":[{\"type\":\"message\",\"replyToken\":\"231995af68a84d8bbcbc9002c1596995\",\"source\":{\"userId\":\"U3b2ac9aa961870f43d718280c6c531b8\",\"type\":\"user\"},\"timestamp\":1539747821507,\"message\":{\"type\":\"text\",\"id\":\"8728732107476\",\"text\":\"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88\"}}]}"

なんかそれっぽいのですが、よくわかりませんでした。



ヘッダーも出力してみた.


header = request.headers ← ※完成したものには関係ないです。

"#<ActionDispatch::Http::Headers:0x00007f5824008c18>"

なんのこっちゃさっぱりわからなかったので教えて欲しいです。


request.body.read のreadに関してはまだ調べられていないので、もしパッとみて分かると思う人がいれば教えて欲しいです。


【4個目】client.parse_events_from(body)とは何だ?

とにかく元の情報を眺めてみる。


line-bot-sdk-ruby/lib/line/bot/client.rb

 def parse_events_from(request_body)

json = JSON.parse(request_body)

json['events'].map { |item|
begin
klass = Line::Bot::Event.const_get(item['type'].capitalize)
klass.new(item)
rescue NameError => e
Line::Bot::Event::Base.new(item)
end
}
end


request_bodyというJSONをparseして、次に、Klassに取得した定数(イベントタイプ)を取得して、SDKに定義されているものであれば、新しいインスタンスを生成している。。。。あっているか心配、、、

const_get

const_get(module)

const_getメソッドは、クラスやモジュールの定数の値を返す。

引数nameには定数名を:NAMEや"NAME"のようにシンボルか文字列で渡す。

存在しない定数名を渡すと例外NameErrorが発生する。

親クラスやインクルードしたモジュールで定義されている定数の値も取り出すことができます。


eventsを出力してみる.


events = client.parse_events_from(body) ← これ

"[#<Line::Bot::Event::Message:0x00000000048c67a8 @src={\"type\"=>\"message\", \"replyToken\"=>\"3c7c713f3827472693cf1f388a38ffae\", \"source\"=>{\"userId\"=>\"U3b2ac9aa961870f43d718280c6c531b8\", \"type\"=>\"user\"}, \"timestamp\"=>1539749189750, \"message\"=>{\"type\"=>\"text\", \"id\"=>\"8728824608853\", \"text\"=>\"テスト\"}}>]"



【5個目】validate_signatureとは何だ?(調査中)

発表までに追いつきませんでした。。

次回発表内容か、、、


line-bot-sdk-ruby/lib/line/bot/client.rb

 def validate_signature(content, channel_signature)

return false if !channel_signature || !channel_secret

hash = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, channel_secret, content)
signature = Base64.strict_encode64(hash)

variable_secure_compare(channel_signature, signature)
end



署名を検証するの処理を調べてみる(調査中)


kiyomiya_bot_controller.rb

def first

body = request.body.read
signature = request.env['HTTP_X_LINE_SIGNATURE']
unless client.validate_signature(body, signature)
error 400 do 'Bad Request' end
end

X-Line-Signatureとはなんだろう?

署名を検証する

「リクエストヘッダーに含まれる署名を検証して、リクエストがLINEプラットフォームから送信されたことを確認する。」とあります。


■メッセージイベントからテキストってどうやって取り出すのか

公式レファレンスに記載されているWEBフックオブジェクト

{

"events": [
{
"replyToken": "0f3779fba3b349968c5d07db31eab56f",
"type": "message",
"timestamp": 1462629479859,
"source": {
"type": "user",
"userId": "U4af4980629..."
},
"message": {
"id": "325708",
"type": "text",
"text": "Hello, world"
}
},
{
"replyToken": "8cf9239d56244f4197887e939187e19e",
"type": "follow",
"timestamp": 1462629479859,
"source": {
"type": "user",
"userId": "U4af4980629..."
}
}
]
}

ここから"message"の"text"だけをピンポイントで取得しようと思ったのですが、どうやったら取得できるのかわからず困りました。どうにかしなきゃということでまずは、サーバーログの確認してみました。

heroku logs -t

このコマンドを打つと、Herokuにデプロイしているアプリのログが取れるようです。

早速出力してみると、、そんな出力するコードを書いていないが、なぜか出力されてる!!!!

こんな感じ。

image.png

毎回、メッセージを変えつつ確認すると、一部分がメッセージによって変わってる!

まずは取り出すの毎回エラーしながら確認するのめんどくさいので、terminalからコピーして別のRubyファイルにペーストし、テキスト取得部分だけ書いてみる。

こんな感じでとれた

(この時、深いネストの配列、ハッシュからのデータ取り出しはやったことなかったので少し感動しました。)

aaaaa["events"][0]["message"]["text"]

でも待てよ、aaaaaの部分って何になるの?? 

これまで、インスタンス変数[:●●]とかで取っていたのですが、今回わからなかっった。。

ここで、数時間が過ぎる

あ、Paramsが使えるんではないのか???(思いつき)

だってWEBフォームっぽいし。

そこで、コードの中に


sample.rb

p "ここから#{params}ここまで"


を記述してみました。

すると、、、取れちゃいました。

ここから

///////中略///////
"message": {
"id": "325708",
"type": "text",
"text": "本塁打" ← キタァー!!_:(´ཀ`」 ∠):
}
ここまで

なのであとはparams[]...と書くだけで良いのですが、、やっぱりparamsってどこでデータを持っているのかわからないので困ります。

これも今後調べて、どういうものか把握したいと思います。(発表もできれば!)

こうして、清宮幸太郎_BOTは何とか動くようになりました。