RailsアプリにLINEBOTを導入して応答パターンを作成する。

# 初めに
初学者です。

個人開発でRailsアプリにlinebotを導入した際の記録です。

食事内容と排便の状態を記録することで、自身の胃腸と食事の相性を判定するツールで、ブラウザで行う操作をlinebotから手軽に行えるようにするために導入しました。
この記事ではLINEBotの初期設定から開始し、アプリに必要な応答パターンを作成するところまでを記録しています。

この記事で行なっている作業はこちらの記事で構築した環境で実施しています。
https://qiita.com/tkhero555/items/a1811369c59021077d62

アプリ「Puri.log」はこちら(2024/5/2時点でMVPリリース状態)
https://purilog-2a85943c9f18.herokuapp.com/


## 技術構成とバージョン
| カテゴリ | 技術 |
|---|---|
| フロントエンド | javascript/Hotwire/bootstrap　|
| バックエンド | ruby3.3.0/rails7.1.3.2 |
| データベース | PostgreSQL16.2 |
| 認証 | devise |
| 環境構築 | Docker / docker-compose |
| CI/CD | Github Actions |
| インフラ　| heroku |


## LINEBOTの初期設定
アカウント登録〜取り敢えずトーク画面で返信設定をするところまでは下記リンクの記事を参考に実施済み
https://qiita.com/kohki_takatama/items/e19960e479a712d63408

## line-bot-apiのインストール〜ngrok設定
Gemfileに追記
```
gem 'line-bot-api'
```
docker composeで環境作ってるのでコマンドをつけながら、gemのインストール、イメージのビルド、linebot用のコントローラー作成まで
```
docker compose run web bundle install
docker compose build
docker compose run web bundle exec rails g controller LineBot
```

これで成功
```
[+] Creating 1/0
 ✔ Container runteq_graduation_exam-db-1  Running                             0.0s 
      create  app/controllers/line_bot_controller.rb
      invoke  erb
      create    app/views/line_bot
      invoke  test_unit
      create    test/controllers/line_bot_controller_test.rb
      invoke  helper
      create    app/helpers/line_bot_helper.rb
      invoke    test_unit
```

ルーティングを設定する
```
# config/routes.rb
Rails.application.routes.draw do
  post 'callback' => 'line_bot#callback'
end
```
作成されたコントローラーにcallbackアクションを用意する。
```
class LineBotController < ApplicationController
  def callback
  end
end
```

## LINEのチャネルとRailsの連携
LINE developersのチャネル管理画面で3つの項目を確認する。
- channel secret
- assertion signing key
- channel access token

railsアプリにdotenv-rails gemを入れて環境変数の定義ができるようにする。
```
gem 'dotenv-rails'
```
バンドルインストールとイメージビルド
```
docker compose run web bundle install
docker compose build
```
.envファイルをルートに作成して、先ほどLINE developersで確認した項目を設定する。
```
LINE_CHANNEL_SECRET='xxxxxxxx'
LINE_CHANNEL_TOKEN='xxxxxxxx'
```

line-bot-apiに用意されているクラスLine::Bot:clientインスタンスを作っておく。
```
class LineBotController < ApplicationController

  def callback
  end

  private
 
 # clientメソッド
  def client
  # インスタンス変数がからであればLine::Bot::Clientのインスタンスを代入する
  # 環境変数を利用して、チャンネルシークレットとトークンをインスタンスに渡している。
    @client ||= Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
  end
end
```

ローカルで開発している際にlinebotの動作を確認するためにngrokを使用する。
サイトにアクセスしてサインアップしてログインを済ませる。
https://ngrok.com/

ローカルでhomebrewを使ってngrokをインストールする。
```
brew install ngrok
```

ngrokのサイトにアクセスし、メニューからyour authtokenを確認する。
PCとngrokアカウントを連携する。
```
ngrok authtoken 確認したトークン
```

ローカルサーバーを起動する。
```
docker compose up
```
ポート番号を指定してngrokを起動する。
```
ngrok http 3000
```
表示されるngrokの画面でForwardingの部分がアクセス先のurl

もう一点、railsのセキュリティ設定を変更してやる必要がある。

```config/enviroments/development.rb
Rails.application.configure do
    # 省略
    config.hosts.clear
  end
```

ローカルサーバーで起動したアプリにngrokを通して、外部からアクセスできるようになった。


## linebotでおうむ返しができるところまで
LINE developersにアクセスして、messaging APIのwebhookURLに、ngrokで生成したURL + /callbackを設定する。

コントローラーに確認用コードを追加する。
ユーザーがbotに送ったメッセージを表示するコードをcallbackアクションに追記
```
class LineBotController < ApplicationController
  def callback
    
    puts "======="
    puts body = request.body.read
    puts "======="
  end

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

linebotにメッセージを送ってみるとエラー
```
ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.):
```
line_bot_controllerに下記を追加して解決
```
skip_before_action :verify_authenticity_token
```
再度メッセージを送って見ると、サーバーログでパラメーターが確認できる。
```
=======
# メッセージから取得したパラメータ
=======
```

callbackアクションをおうむ返しするものにしていく
```
def callback
# LINEからのイベントデータを取得する
    body = request.body.read
    # gemのメソッドであるparse_events_from(body)でevents配列を取得する。
    events = client.parse_events_from(body)
    # 取得した配列を繰り返しで処理する
    events.each do |event|
    # caseを使用して、eventの種類の条件をwhenで指定して、種類ごとに処理を行う。
      case event
      # eventがMessageかどうかをチェック
      when Line::Bot::Event::Message
      # ネストしてcaseを使用、eventのさらにタイプで分類
        case event.type
        # MessageType::Textかどうかをチェック
        when Line::Bot::Event::MessageType::Text
        # messageに受け取ったメッセージをそのまま代入する。
          message = {
            type: "text",
            text: event.message["text"]
          }
          # これがユーザーにメッセージを配信する記述２つ目の引数にあるmessageに先ほど代入した内容が入っている。
          client.reply_message(event['replyToken'], message)
        end
      end
    end
  end
```

送ったメッセージをそのまま返してくるようになってれば成功

## 必要なパターンを整理する。
必要なやり取りのパターン

- ユーザー「食事の記録」　bot「食事名を送ってください。例：〜」
- ユーザー「食事名」　bot「日時に食事名を食べたことが記録されました。」　（railsに入力データを引き渡す）
- ユーザー「便の記録」　bot「便の状態を選んでください。1:良い, 2:普通, 3:悪い」　ユーザー　選択肢クリック　bot「日時に状態の便が記録されました。」　
- ユーザー「おすすめの食事」　bot「あなたにおすすめの食事は〜です。」
- ユーザー「避けるべき食事」　bot「あなたが避けるべき食事は〜です。」

メッセージイベントのテキストであるかを既存の分岐で判断した上で、メッセージのテキストの内容を変数に格納し、格納したテキストの内容との一致で分岐を作って処理をする。
パターン一つずつ機能を作っていき、条件分岐を加えていく。
データベースへの記録を担うコードは、別途実装する予定。


## パターンごとに実装
### メッセージかどうかの判別
```
body = request.body.read
    events = client.parse_events_from(body)
    events.each do |event|
      case event
      when Line::Bot::Event::Message
        case event.type
        when Line::Bot::Event::MessageType::Text
	        case event.message["text"]
	        when "食事の記録"
	        when "便の記録"
	        when "おすすめの食事"
	        when "避けるべき食事"
	        else
	      end
```

### ユーザー「食事の記録」　bot「食事名を送ってください。例：〜」　
リッチメニューをクリックして、ユーザーに「食事の記録」とメッセージを送らせる想定なので、 == ’食事の記録’ を条件式とする。
```
when "食事の記録"
	message = {
    			type: "text",
				text: "メッセージで食べたもののメニュー名を送ると記録されます。\n送った際の時刻に食べたものとして記録されます。\n時間指定をして記録したい場合は、サイトから実行してください。"
						}
	client.reply_message(event['replyToken'], message)
```

### ユーザー「食事名」　bot「日時に食事名を食べたことが記録されました。」
これが唯一入力形式を絞り込めない項目になるので、他の条件に合致しない場合にこの動作をするように設定する。
```
else
  meal_log = event.message["text"]
=begin
  データベースへの保管を行うコードを実装予定
=end
  message = {
             type: "text",
             text: "食事の記録が完了しました。" 
            }
  client.reply_message(event['replyToken'], message)
```

### ユーザー「便の記録」　bot「便の状態を選んでください。1:良い, 2:普通, 3:悪い」　
判別は”便の記録”
選択肢つきのメッセージを作成して、ユーザーに返信する。
選択肢をクリックすると、数字の1~3をユーザーが送る。
app/services/line_bot/messages/フォルダを用意して、unko_message.rbを作成し、ボタンテキストのコードを記述することにする。
```app/services/line_bot/messages/unko_message.rb
module LineBot
  module Messages
    class UnkoMessage
      def button_message
        {
          "type": "template",
          "altText": "This is a buttons select stools condition",
          "template": {
            "type": "buttons",
            "title": "排便の記録",
            "text": "選択肢から便の状態を選択してタップしてください。",
            "actions": [
              {
                "type": "message",
                "label": "1:良い",
                "text": "1"
              },
              {
                "type": "message",
                "label": "2:普通",
                "text": "2"
              },
              {
                "type": "message",
                "label": "3:悪い",
                "text": "3"
              }
            ]
          }
        }
      end
    end
  end
end
```

```line_bot_controller.rb
when "便の記録"
  message = LineBot::Messages::UnkoMessage.new.button_message
  client.reply_message(event['replyToken'], message)
```

便の記録と送ると選択肢つきのメッセージが返ってくる。
![スクリーンショット 2024-04-05 18.51.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3768250/30aedfd8-b942-10f0-1591-31aa65daf1b5.png)

### ユーザー　選択肢クリック　bot「排便が記録されました。」　(railsに入力データを引き渡す）
条件式は、1 or 2 or 3
```
when "1", "2", "3"
  puts "便の記録完了確認用"
  stool_log = event.message["text"]
=begin 
  # 便の記録と、状態に応じた評価値の変動
=end
  message = {
              type: "text",
              text: "排便の記録が完了しました。" 
            }
  client.reply_message(event['replyToken'], message)
```

### ユーザー「おすすめの食事」　bot「あなたにおすすめの食事は〜です。」
判別は”おすすめの食事”
データベースからデータと取って動的にテキストを構築してユーザーに送る。
見つからない場合は、”おすすめの食事はありません”と表示する。
データベースを使用するコードは別途実装するので、この時点では"おすすめの食事機能は未実装です。"と返信するようにしておく。
```
 when "おすすめの食事"
   recommend_meal = "おすすめの食事機能は未実装です。"
　　　　　　　message = {
            　　　　　type: "text",
            　　　　　text: recommend_meal
            　　　　}
　　　　　　client.reply_message(event['replyToken'], message)
```

### ユーザー「避けるべき食事」　bot「あなたが避けるべき食事は〜です。」
おすすめの食事と同じ手法で実装する。
```
 when "避けるべき食事"
   avert_meal = "避けるべき食事機能は未実装です。"
　　　　　　　message = {
            　　　　　type: "text",
            　　　　　text: avert_meal
            　　　　}
　　　　　　client.reply_message(event['replyToken'], message)
```

これで必要な応答パターンをlinebotに実装できた。
コードを整理してline_bot_controllerを完成させる。
```line_bot_controller.rb
class LineBotController < ApplicationController

  def callback
    events.each do |event|
      client.reply_message(event['replyToken'], message(event))
    end
  end

  private
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
  end

  def request_body
    request_body = request.body.read
  end

  def events
    # gemのメソッドであるparse_events_from(body)でevents配列を取得する。
    events = client.parse_events_from(request_body)
  end

  def message(event)
    # caseを使用して、eventの種類の条件をwhenで指定して、種類ごとに処理を行う。
    case event
    # eventがMessageかどうかをチェック
    when Line::Bot::Event::Message
    # ネストしてcaseを使用、eventのさらにタイプで分類
      case event.type
      # MessageType::Textかどうかをチェック
      when Line::Bot::Event::MessageType::Text
        case event.message["text"]
        when "食事の記録"
          message = {
                      type: "text",
                      text: "メッセージで食べたもののメニュー名を送ると記録されます。" +
                      "送った際の時刻に食べたものとして記録されます。" +
                      "時間指定をして記録したい場合は、サイトから実行してください。"
                    }

        when "便の記録"
          message = LineBot::Messages::UnkoMessage.new.button_message

        when "1", "2", "3"
          puts "便の記録完了確認用"
          stool_log = event.message["text"]
=begin 
          便の記録と、状態に応じた評価値の変動
=end
          message = {
                      type: "text",
                      text: "排便の記録が完了しました。" 
                    }

        when "おすすめの食事"
          puts "おすすめの食事動作確認用"
=begin 
          データベース操作のコード
=end
          recommend_meal = "おすすめの食事機能は未実装です。"
          message = {
                      type: "text",
                      text: recommend_meal
                    }

        when "避けるべき食事"
          puts "避けるべき食事動作確認用"
=begin 
          データベース操作のコード
=end
          avert_meal = "避けるべき食事機能は未実装です。"
          message = {
                      type: "text",
                      text: avert_meal
                    }

        else
          meal_log = event.message["text"]
=begin
          データベースへの保管を行うコード
=end
          message = {
                      type: "text",
                      text: "食事の記録が完了しました。" 
                    }
        end
      end
    end
  end
end
```

#　参考記事など
https://qiita.com/kohki_takatama/items/e19960e479a712d63408
https://qiita.com/KNR109/items/e1b5ebd94393441fff74
https://qiita.com/nishina555/items/4ffaf5cc57a384b66230
https://qiita.com/4geru/items/0b8a19165e5b694f5446
https://qiita.com/Yuzu_Ginger/items/5e5fbe6d61e1abe2b8ab
https://developers.line.biz/ja/reference/messaging-api/#action-objects
https://qiita.com/4geru/items/0b8a19165e5b694f5446