【Rails】初めてのLINE Bot(Gem line-bot-api)

LINEで、返答を返してくれるチャットボット（DBからのデータ表示、スクレイピング）を実装しました。前準備のLINEアカウント作成手順については、[LINE Developers](https://developers.line.biz/en/)などに丁寧に記載されているため割愛。

# 概要
私事ですが、通勤に電車と会社のバスを使っています。
そこで、自分用に次に乗れそうなバスの時刻とスクレイピングで電車の運行状況を返してくれるLINE Botを実装してみました。
**※ スクレイピングはスクレイピング先に負荷をかける恐れがあるため、実際の運用はせず、勉強のための実装です。**
![Alt text](https://gyazo.com/20ff99abbb1deda5e9d0262e73a0cad6/raw)

# 手順
- [LINE Developers](https://developers.line.biz/en/)にボット作成のためのアカウントを作成（割愛）。
- 実装中に動きを確認する目的で、ローカルサーバー3000で動かせるよう、[Ngrok](https://ngrok.com/download)を導入。
- `gem 'line-bot-api'`で、実装。

## Ngrok
- localhostで動くサーバーを、LAN外からアクセスを可能にする。
- ローカルPC上のサービスを外部公開することが可能。

今回は、ローカル環境で立ち上げたポート番号3000番を外部からアクセスできるようにしたい。

**使用方法**
① ダウンロード

```
$ brew cask install ngrok
```
② ngrokを起動
今回はport番号：3000を指定して、ngrokを起動する。

```
$ ngrok http 3000(port番号)
```
立ち上がると、ターミナルに表示された下記の`https://乱数.ngrok.io`の部分をコピーしておく。
**※ 乱数の部分は、サーバーを起動する度に変わるので、その度に②以降の作業が必要。**

```
    :
Forwarding    https://乱数.ngrok.io -> http://localhost:3000
    :
```
ブラウザで`http://localhost:3000/`に行くと、railsサーバーが立ち上がること確認できる。※もちろん、アプリも`rails s`で起動しておく必要がある。

③ LineBotのWebhookの設定
[LINE Developers](https://developers.line.biz/en/)のBotチャネルのWebhook URLの箇所を、上記②のURL + /callback( 例 `https://乱数.ngrok.io/callback`）に変更する。
※ Webhook ： 一言で言うと、Botにイベントが発生した時、Botインスタンスに通知するためのアクセスポイント（URL）の事。そのURLを指定して、LINEからPOSTリクエストが送信される。

④ 接続確認
あとは、Botを友達追加し、動作確認ができる。

## コード
DBはmysqlを使用。

```
$ rails new アプリ名 -d mysql
```


dbを作成し、シャトルバスの時刻表テーブルを作成。

```
$ rails db:create
```

gemを導入 → `bundle install`

```ruby:Gemfile
gem 'line-bot-api'
gem 'nokogiri'
```

### ルーティング

```ruby:routes.rb
post '/callback', to: 'linebot#callback'
```

### コントローラー

```ruby:application_controller.rb
# gem 'line-bot-api'を使えるように宣言
require 'line/bot'

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

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

  def client
    @client ||= Line::Bot::Client.new { |config|
      # ローカルで動かすだけならベタ打ちでもOK。
      config.channel_secret = "your channel secret"
      config.channel_token = "your channel token"
    }
  end
end
```
linebotコントローラーを作成

```
$ rails g controller linebot
```

```ruby:linebot_controller.rb
class LinebotController < ApplicationController
  protect_from_forgery except: :sort

  # ルーティングで設定したcallbackアクションを呼び出す
  def callback
    body = request.body.read
    events = client.parse_events_from(body)

    events.each { |event|
      require "date"
      require 'nokogiri'
      require 'open-uri'

      #時刻表示を 時:分 に指定
      now = DateTime.now
      nowTime = now.strftime("%H:%M")
         :
     # 下記に記載
         :
      case event
      when Line::Bot::Event::Message
        case event.type
        when Line::Bot::Event::MessageType::Text
          message = {
            type: 'text',
            text: response
          }
          client.reply_message(event['replyToken'], message)
        when Line::Bot::Event::MessageType::Image, Line::Bot::Event::MessageType::Video
          response = client.get_message_content(event.message['id'])
          tf = Tempfile.open("content")
          tf.write(response.body)
        end
      end
    }
    "OK"
  end
end
```

```ruby:linebot_controller.rb
    :
# 1 を入力した時のアクション(DBからデータ取得)
if event.message["text"].include?("1")
  nextBus = BusTimetableKaiseiSt.all
  nextBusKaisei = []
  nextBus.each do |nextBus|
    time = nextBus.time.strftime("%H:%M")
    if time >= nowTime
      nextBusKaisei << time
    end
end
#DBから現在時刻を起点に直近の３つのバスの時刻を出力
response = 
    "開成発"+nextBusKaisei[0]+"\n
    Next "+nextBusKaisei[1]+"\n
    "+nextBusKaisei[2]+"\n\n\n
    ↓↓番号を選択↓↓\n
    1. 開成駅→会社（シャトルバス）\n
    2. 会社→開成駅（シャトルバス）\n
    3. 電車の運行状況\n
    4. 会社周辺の天気\n
    5. 東京の天気\n\n
    ※半角数字でお願いします。"

# 2 を入力した時のアクション(DBからデータ取得)
  # 流れは上記と同様なので、割愛

# 3 を入力した時のアクション(スクレイピングでデータ取得)
elsif event.message["text"].include?("３")
  urlOdakyu = 'https://www.odakyu.jp/cgi-bin/user/emg/emergency_bbs.pl'
  charset = nil
  htmlOdakyu = open(urlOdakyu) do |f|
    charset = f.charset
    f.read
  end

  docOdakyu = Nokogiri::HTML.parse(htmlOdakyu, nil, charset)
  docOdakyu.xpath('//div[@id="pagettl"]').each do |node|
    #スクレイピング情報の出力
    response = 
      node.css('p').inner_text+"\n\n\n
      ↓↓番号を選択↓↓\n
      1. 開成駅→会社（シャトルバス）\n
      2. 会社→開成駅（シャトルバス）\n
      3. 電車の運行状況\n
      4. 会社周辺の天気\n
      5. 東京の天気\n\n
      ※半角数字でお願いします。"
  end

# 4 を入力した時のアクション(スクレイピングでデータ取得)
  # 方法は上記と同様なので、割愛

# 5 を入力した時のアクション(スクレイピングでデータ取得)
  # 方法は上記と同様なので、割愛

# 上記以外を入力した時のアクション
else
  response =
      "↓↓番号を選択↓↓\n
      1. 開成駅→会社（シャトルバス）\n
      2. 会社→開成駅（シャトルバス）\n
      3. 電車の運行状況\n
      4. 会社周辺の天気\n
      5. 東京の天気\n\n
      ※半角数字でお願いします。"
end
       :
```