【Rails】ユーザーの状態に応じた対話機能をLINEbotで実現する【LINE Messaging API】

# 概要

本記事は [RUNTEQアドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/runteq) の7日目の記事となります！

個人開発で**ゴミ捨て日を通知してくれるLINEアプリ**を開発したのですが、
その中で実装するのに苦労した「**ユーザーの状態を把握する**」方法を記事にします。

一般的なRailを使用したWebアプリケーションでは、Cookieを利用したユーザー識別やフォームを利用してユーザーの送信する内容を一挙に受け取るなどができますが、LINE Messaging APIではこれらが使えません...

そこで私が実装した、Railsのみでユーザーの状態を保持する方法を紹介します。

これからRails x LINE Messaging API を試してみたい方の参考になれば幸いです。

※ LINE Developperへの登録や設定、基本的な実装についての概要などは他に多数の素晴らしい記事があるため、割愛させていただきます。([こちらの記事](https://qiita.com/Tsuchiy_2/items/4e8c038f58c23b57b0be)など)
※ 私はいわゆる未経験エンジニアですので、あまり綺麗なコードではないかもしれませんし、他にもっといい方法があるかもしれません。マサカリ歓迎です。

# どんな実装にしたかったか？

まず、どんな実装にしたかったかを説明します。

ユーザーがゴミ捨ての予定を登録するという作業で、
「ゴミ捨ての予定を次々と質問され、それに答えていくと登録できる。」
というシナリオで実装したいと思いました。

具体的には、
下図のようにゴミの名前・収集する曜日・周期を答えていき

<img width="300" alt="scenario_1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1582215/c8d61874-6485-4874-81df-e7388c38345e.png">

最後に通知してほしい時間を答えれば、ゴミ捨ての登録完了！

という流れです。

<img width="300" alt="scenario_2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1582215/2e8ad8e5-dfe4-09e5-e2ae-328ba13f0f6d.png">


しかし、LINEユーザーから送られてくる情報は主に

1. ユーザーの識別情報
2. 送信したテキスト

の2つのため、ユーザーの状態の把握（セッション管理）まではできません。

例えば、「1」というテキストが送られてきただけでは「毎週」なのか「月曜日」なのか分からないということです。

# 実装

結論から言うと、Userモデルに対話状態を表すstatusカラムを持たせ、対話内容を記録するMessageモデルを作成しました。

コードで説明していきます。

## Model

Userモデルに状態を管理するstatusカラムを追加しました。

```user.rb
class User < ApplicationRecord
  
  ...

  has_many :messages, dependent: :destroy

  enum status: {
    top: 0,
    registration: 1,
    show_next: 2,
    add_day_of_week: 3,
    add_cycle: 4,
    add_notification: 5
  }
end
```

Messageモデルを作成し、Userモデルに対し1対多の関連付けをしました。

```message.rb
class Message < ApplicationRecord
  belongs_to :user
end
```

※ 他のモデルについては省略します

## Controller

コントローラーの内容は少し複雑かと思いますので、4段階に分けて説明していきます。

### 1. 基本形

まずは基本的なオウム返しの実装です。

main_actionメソッドの中でユーザーの返信内容などによって条件分岐させることによって、
レスポンスメッセージを書き換えることができます。

これ以降はmain_actionメソッドに注目し、書き加えていきます。

```linebot_controller.rb
# オウム返しする LINEbot
class LinebotController < ApplicationController
  require 'line/bot'

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

  def callback
    body = request.body.read

    signature = request.env['HTTP_X_LINE_SIGNATURE']
    head :bad_request unless client.validate_signature(body, signature)

    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
          # 条件分岐
          main_action(event)

          message = {
            type: 'text',
            text: @response
          }
          client.reply_message(event['replyToken'], message)
        end
      end
    }

    head :ok
  end

  # この中身だけ書き換える
  def main_action(event)
    # ユーザーの返信したメッセージ
    text = event.message['text']

    @response = text
  end
end
```

### 2. user の 状態によって返信内容を変える

いちいちupdateが走ってしまいますが、やり取りの度にstatusカラムの値を更新して状態を保持します。

これによって、「ゴミの名前は？」「曜日は？」と言ったように、レスポンスを投げる順序を制御できます。

```linebot_controller.rb
class LinebotController < ApplicationController

  ...

  def main_action(event)
    # ユーザーの識別
    @user = User.find_or_create_by(line_id: event['source']['userId'])
    text = event.message['text']

    # statusカラムの値で分岐
    case @user.status
    when 'top'
      case text
      # 1 が返信されたら、登録モードへ
      when '1'
        @response = "登録モードへ移行します。\nゴミの名前を入力してください。"
        @user.registration!
      end
    ### 登録モード ###
    when 'registration'
      @response = '曜日を入力してください。'
      @user.add_day_of_week!
    when 'add_day_of_week'
      @response = '周期を入力してください。'
      @user.add_cycle!
    when 'add_cycle'
      @response = '通知する時間を入力してください。'
      @user.add_notification!
    when 'add_notification'
      @response = '登録完了です。'
      # ゴミの登録が完了したら、topに戻す
      @user.top!
    end
  end
end
```

### 3. ユーザーの返信内容を保存する

ユーザーのいくつかの返信を組み合わせてゴミ捨て日のレコードを生成したいため、以前のユーザーの返信内容を保存しておく必要があります。

ここでMessageモデルを使用します。

以下のコードのように、ユーザーが返答した内容（ゴミの名前など）をDBに保存しておきます。

そして、登録完了メッセージと同時にゴミを確定し、ゴミのレコードを生成します。

```linebot_controller.rb
class LinebotController < ApplicationController

  ...

  def main_action(event)
    @user = User.find_or_create_by(line_id: event['source']['userId'])
    text = event.message['text']

    case @user.status
    when 'top'
      case text
      when '1'
        @response = "登録モードへ移行します。\nゴミの名前を入力してください。"
        @user.registration!
      end
    ### 登録モード ###
    when 'registration'
      @user.messages.create!(text: text) # ゴミの名前を覚えておく
      @response = '曜日を入力してください。'
      @user.add_day_of_week!
    when 'add_day_of_week'
      @user.messages.create!(text: text) # ゴミの曜日を覚えておく
      @response = '周期を入力してください。'
      @user.add_cycle!
    when 'add_cycle'
      @user.messages.create!(text: text) # ゴミの周期を覚えておく
      @response = '通知する時間を入力してください。'
      @user.add_notification!
    when 'add_notification'
      # ここでゴミレコードを生成する
      @response = '登録完了です。'
      @user.top!
    end
  end
end
```

### 4. 完成！

ここまでの知識とその他諸々を組み合わせ、完成したコードです。

これで最初の写真のようなやり取りができるようになります！

```ruby
# main_action 完成形
class LinebotController < ApplicationController

  ...

  def main_action(event)
    @user = User.find_or_create_by(line_id: event['source']['userId'])
    text = event.message['text']
                      .tr(" 　\r\n\t", '') # 空白の除去
                      .tr('０-９', '0-9')  # 全角数字を半角に

    case @user.status
    when 'top'
      case text
      # 1 が返信されたら、登録モードへ
      when '1'
        @response = <<~TEXT
          ゴミの名前を何にする？一つだけ答えてね！
          (例)燃えるゴミ
        TEXT

        @user.registration!
      end
    ### 登録モード ###
    when 'registration'
      # ゴミの名前を保存！
      @user.messages.create!(text: text)

      @response = <<~TEXT
        収集日はいつにする？
          1: 月曜日
          2: 火曜日
          3: 水曜日
          4: 木曜日
          5: 金曜日
          6: 土曜日
          7: 日曜日
          0: ゴミの登録をやめる\n
      TEXT

      @user.add_day_of_week!
    when 'add_day_of_week'
      # 収集する曜日を保存！
      @user.messages.create!(text: text)

      @response = <<~TEXT
        周期はどうする？
          1: 毎週
          2: 今週から隔週
          3: 来週から隔週
          4: 第１・３
          5: 第２・４
          0: やめる
      TEXT

      @user.add_cycle!
    when 'add_cycle'
      # 収集する周期を保存！
      @user.messages.create!(text: text)

      @response = <<~TEXT
        何時に通知する？
        10分単位で設定できるよ！
          (例1)6:40
          (例2)7時20分
          (例3)8時半

          0: ゴミの登録をやめる
      TEXT

      @user.add_notification!
    when 'add_notification'
      text.gsub!(/時|分|半/, '時' => ':', '分' => '', '半' => '30')
      # 00:00-23:50のフォーマットに則っているかどうかの判定
      if text.match(/^([01]?[0-9]|2[0-3]):[0-5]0$/)
        # やり取りしたゴミの情報を引き出す
        trash_name = @user.messages[-3].text
        day_of_weeks = @user.messages[-2].text
        cycle_num = @user.messages[-1].text
        collection_days = CollectionDay.find(day_of_weeks)
        cycle = Cycle.find(cycle_num)

        # ゴミと通知時間を作成する
        trash = @user.trashes.create!(
          name: trash_name,
          cycle: cycle,
          collection_days: [collection_days].flatten
        )
        Notification.create!(trash: trash, notify_at: text)

        @response = <<~TEXT
          登録したよ！
          「#{trash.name}」の収集日は
          「#{trash.cycle.name_i18n}」の「#{trash.collection_days_list}」
          「#{l trash.notification.notify_at}」に通知するからね！
        TEXT

        @user.top!
      else
        @response = '正しく入力してね！'
      end
    end
  end
end
```

# 最後に

ここまで読んでいただき、ありがとうございます。

以上が私の実装したコードの一部です。

段々長くなっていくコードで読みづらいとは思いますが、、、みなさまの参考になれば幸いです。

---

### 参考

[今更ながらRails5+line-bot-sdk-ruby+HerokuでLineBot作成してみたら、色々詰まったのでまとめました。
](https://qiita.com/y428_b/items/d2b1a376f5900aea30dc)

[【LINE×Rails】Rails初学者も作れるLINE Botアプリケーション](https://qiita.com/Tsuchiy_2/items/4e8c038f58c23b57b0be)