Ruby on RailsのアプリにLINEを組み込む。

## はじめに
- 今回は[【個人開発】日用品を自動で購入してくれるアプリを作りました。](https://qiita.com/prg_mt/items/decb7912e82e6384fafb)で実装したLINEログインとLINEbotについての解説記事になります。
- 連載企画の続編[「RubyonRailsにスクレイピング機能を持たせてみる。」](https://qiita.com/prg_mt/items/5383d87fccdd7fc2cecf)を書きました!


## Lineログイン機能について
### Railsアプリの作成
まずはじめに アプリを作成していない方は`rails new`してアプリを作成してください。

### Deviseの導入
LINEログイン機能を実装する前にまずはDeviseでログイン機能を実装していきます。

```ruby:Gemfile
gem 'devise'
```

Gemfileに記載が完了したら、`bundle install`を実行

次に、ターミナルで以下を実行し、Deviseを使えるようにしていきます。
```terminal:ターミナル
rails g devise:install
rails g devise User
rails db:migrate
```

ここまで問題なく実行できれば、`rails s`でサーバを立ち上げhttp://localhost:3000/users/sign_in にアクセスし、ログイン画面が表示されているか確認してください。

### LINE Developersの登録
ここからいよいよLineログインの機能を組み込んでいきます。
まずhttps://developers.line.biz/ja/ にアクセスし、右上のログインからビジネスアカウントでログインを選びアカウントを作成してください。
![スクリーンショット 2023-01-04 13.03.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/2e92ce3d-8c6b-9d6d-9369-ec47b24a4298.png)
無事にログインができると上記のような画面に移りますので、プロバイダーを作成します。
プロバイダー名はややこしくなるといけないので、自分のアプリの名前にするとわかりやすいです。
![スクリーンショット 2023-01-04 13.05.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/4a3d75c2-012a-b414-b91b-6e35e5e100ed.png)
作成が完了するとこのような画面になりますので、LINEログインを選択します。
新規チャネル作成画面になりますので、必須項目を記載し、作成ボタンをクリックします。

### LINEログイン機能の導入
Gemfileに以下を記載し、`bundle install`を実行。
```ruby:Gemfile
gem 'omniauth-line'
gem 'omniauth-rails_csrf_protection'
```
また、環境変数を設定するため下記のgemも追加します。
```ruby:Gemfile
gem 'dotenv-rails'
```

上記のgemを追加したら、アプリのルート直下に`.env`ファイルを作成し、以下の2つを追加します。
```.env
LINE_KEY='自身のChannel ID'
LINE_SECRET='自身のChannel Secret'
```
![スクリーンショット 2023-01-04 13.14.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/0ce2c471-fba2-f399-2ca0-b46e49d88e2c.png)
![スクリーンショット 2023-01-04 13.17.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/67033566-9279-944a-6a63-fcb9cfff537f.png)
次に、`.env`ファイルをGitの管理から外すために`.gitignore`に以下を追加。
```.gitignore
/.env
```

### DeviseとLINEログインの連携
Deviseの設定ファイルにLINEログインの設定を行います。
`devise.rb`に以下を追加
```ruby:config/initializers/devise.rb
config.omniauth :line, ENV['LINE_KEY'], ENV['LINE_SECRET']
```
`User.rb`に以下を追加
```ruby:app/modles/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :omniauthable, omniauth_providers: %i[line] # この1行を追加
end
```

#### omniauth_collbacks_controllerの作成
```terminal:ターミナル
rails g controller omniauth_callbacks
```
omniauth_collbacks_controller内を以下の記述に変更
```ruby:app/controllers/omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  
  def line
    basic_action
  end

  private
  
  def basic_action
    @omniauth = request.env["omniauth.auth"]
    if @omniauth.present?
      @profile = User.find_or_initialize_by(provider: @omniauth["provider"], uid: @omniauth["uid"])
      if @profile.email.blank?
        email = @omniauth["info"]["email"] ? @omniauth["info"]["email"] : "#{@omniauth["uid"]}-#{@omniauth["provider"]}@example.com"
        @profile = current_user || User.create!(provider: @omniauth["provider"], uid: @omniauth["uid"], email: email, name: @omniauth["info"]["name"], password: Devise.friendly_token[0, 20])
      end
      @profile.set_values(@omniauth)
      sign_in(:user, @profile)
    end
    #ログイン後のflash messageとリダイレクト先を設定
    flash[:notice] = "ログインしました"
    redirect_to expendable_items_path
  end

  def fake_email(uid, provider)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```
#### ルーティングの設定
ルーティングの設定を以下のようにします。
```ruby:confing/routes.rb
devise_for :users, controllers: {
  omniauth_callbacks: "omniauth_callbacks"
}
```

#### モデルの変更
以下のコマンドでUserモデルにカラムを追加作成し、LINEログインができるようにします。
```terminal:ターミナル
rails g migration add_column_to_users
```
```ruby:db/migrate/〇〇.rb
class ChangeColumnToUser < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :name, :string, null:false
  end
end
```
最後に`rails db:migrate`でマイグレーションを実行。

#### Userモデルの編集
Userモデルに以下を追加
```ruby:app/models/User.rb
def social_profile(provider)
    social_profiles.select { |sp| sp.provider == provider.to_s }.first
  end

  def set_values(omniauth)
    return if provider.to_s != omniauth["provider"].to_s || uid != omniauth["uid"]
    credentials = omniauth["credentials"]
    info = omniauth["info"]

    access_token = credentials["refresh_token"]
    access_secret = credentials["secret"]
    credentials = credentials.to_json
    name = info["name"]
  end

  def set_values_by_raw_info(raw_info)
    self.raw_info = raw_info.to_json
    self.save!
  end
```
ここまででRails側の設定は完了です。

## コールバックの設定
### LINE DevelopersでコールバックURLの設定
先ほどのチャネルの設定画面のLINEログイン設定タブの中にコールバックURLを入力する箇所があります。ここに、`https(http)://デプロイ先のURL(IPアドレスまたはドメイン)/users/auth/line/callback`を入力するとデプロイしたアプリからLINEログインができるようになります。
![スクリーンショット 2023-01-04 13.46.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/6a14fe3e-9e89-021d-f67e-e893104a0d90.png)
ただし、開発環境で実行する場合にhttp://localhost:3000/users/auth/line/callback のように設定して使うことはできません。
これはlocalhostは外部からアクセスすることができないからです。
外部からのアクセスを許可するためにngrokを使用していきます。

### 開発環境で外部からのアクセスを許可する

https://ngrok.com/

上記サイトにアクセスし、sign upしてngrokをインストールしてください。
set up & installationの項目が全て完了したら下記コマンドが打てるようになるはずです。

```terminal:ターミナル
ngrok http 3000
```

また、Railsサーバーも起動しないといけないのでターミナルを分割して`rails s`でサーバーを起動してください。

【Rails6の場合】
Rails6の場合は以下を追記してngrokのアクセスを許可してください。
```ruby:config/enviroments/development.rb
Rails.application.configure do
  config.hosts.clear
end
```

### チャネルの公開
最後に作成したチャネルを公開します。

上にある非公開ボタンをクリックし、チャネルを公開にしてください。
チャネルが公開に切り替わればLINEログインができるようになります。


## LINEでプッシュ通知を送る
#### LINE DevelopersのLINEログインを作成したプロバイダーにて`Messaging API`を作成します。
画面の指示に従って、必須事項を入力していき作成ボタンをクリックして作成します。
無事に作成できたらMessaging API設定タブの一番下にあるチャンネルアクセストークンを発行しておきます。

#### Rails側の設定
以下を記載し、`bundle install`をします。
```ruby:Gemfile
gem 'line-bot-api'
```
LINEログインの際に作成した`.env`に以下を追記します。
```.env
LINE_CHANNEL_SECRET='channel Secret'
LINE_CHANNEL_TOKEN='Channel access token'
```
![スクリーンショット 2023-01-04 14.19.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/b1def5f0-f510-4ad6-666d-9827c26006c7.png)
![スクリーンショット 2023-01-04 14.20.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/786a62e0-a64d-2721-eaf4-7b78b6a649f8.png)

ここまででbotの設定編は終了です。

### LINEでメッセージを送信する
 ここではActiveJobを用いてメッセージを送信していきます。
 ActiveJob以外でもcontroller内に記載してやることでcreateやupdateアクション時にメッセージを同期的に送ることも可能です。

```ruby:app/job/push_line_job.rb
require 'line/bot'

class PushLineJob < ApplicationJob
  queue_as :default

  def perform(*args)
    limit_seven_days = Date.today..Time.now.end_of_day + (7.days)
    users = User.all
    users.each do |user|
      if user.line_alert == true
        limit_items =  ExpendableItem.where(user_id: user.id).where(deadline_on: limit_seven_days)
        if limit_items != []
          names = limit_items.map {|item| item.name } 
          message = {
                type: 'text',
                text: "1週間以内に#{names.join(',')}が無くなります。早めの買い足しをオススメします。"
              }
          response = line_client.push_message(user.uid, message)
          logger.info "PushLineSuccess"
        end
      end
    end
  end

  private

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

### コードの内容について解説していきます。

gemの`line-bot-api`を使用する際には下記が必要となります。
```
require 'line/bot'
```
下記メソッドの中で、先ほど作成したbotを指定しています。
また、メソッドにすることでmockを使用したテストを行いやすくしています。
```
def line_client
    Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
  end
```
message変数の中に送信するメッセージの種類と内容を記載することでメッセージを作成しています。
```
message = {
                type: 'text',
                text: "1週間以内に#{names.join(',')}が無くなります。早めの買い足しをオススメします。"
              }
```
line_clientというインスタンスにあるpush_messageというインスタンスメソッドを使用してメッセージを送信しています。
引数は第一引数で送り先を指定し、第二引数で上記のメッセージを渡しています。
また、user.uidはLINEログインの際にコールバックで引っ張ってきたLINE上のアカウントIDになります。
```
response = line_client.push_message(user.uid, message)
```

上記コードにより、LINEBOTからユーザにプッシュ通知を送ることができます。

## webhookを利用したリプライ機能の搭載(1/14追記)
また、ユーザから特定の文言がbotに対して送信された際、それを起点としてプログラムを走らせる処理を実装していきます。
完成系は下記となります。
![IMG_6053.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/8128e941-0943-6230-6179-046800ede760.png)

### LINEDevelopersの設定
まず、LINEDevelopersにてwebhookの設定を行います。
![スクリーンショット 2023-01-14 22.55.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/380648e1-bbcd-368b-7ddf-5d4de14593c6.png)
上記のように、MessagingAPI設定の画面にてWebhookの設定を行います。
Webhook URLに自身のアプリの`URL/callback`を設定し、Webhookの利用をONにしてください。
(開発環境でテストを行う際にはLINEログインの際と同じようにngrokのURLを設定してください。)
その下の再送やエラーについての項目については必要に応じて設定を変えてください。

### Rails側のroutesの設定
次に、Webhookから送られてきたリクエストを処理する際に、どこで処理するかをroutesで記載していきます。
```ruby:config/routes.rb
Rails.application.routes.draw do
    post '/callback' => 'linebot#callback'
end
```

今回はLinebotControllerのcallbackメソッドで処理を行いますので、
上記のように、'/callback'にPOSTリクエストが来た際にLinebotControllerのcallbackメソッドが呼び出されるように設定します。

### LinebotControllerの設定
最後に処理の要となるLinebotControllerを作成していきます。
まず、Controllerを作成するので下記コマンドを叩いてください。
```terminal:ターミナル
rails g controller Linebot
```
そうしますと、contoroller以外にもviewなども作成されますが、viewについては今回は使用しないので削除してください。

次に、LinebotController内の記述をしていきます。

```ruby:app/controllers/linebot_controller.rb
require 'line/bot'

class LinebotController < ApplicationController
  # callbackアクションのCSRFトークン認証を無効
  protect_from_forgery :except => [:callback]
  
  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']
    unless client.validate_signature(body, signature)
      error 400 do 'Bad Request' end
    end

    events = client.parse_events_from(body)

    events.each { |event|
      user_id = event['source']['userId']
      user = User.where(uid: user_id)[0]
      if event.message['text'].include?("お買い物リスト")
        message = shopping_list(send_limit_item(user))
      elsif event.message['text'].include?("自動購入機能テスト")
        message = test_selenium(user)
      end

      case event
      # メッセージが送信された場合
      when Line::Bot::Event::Message
        case event.type
        # メッセージが送られて来た場合
        when Line::Bot::Event::MessageType::Text
          client.reply_message(event['replyToken'], message)
        end
      end
    }

    head :ok
  end

  private

  def send_limit_item(user)
    limit_seven_days = Date.today..Time.now.end_of_day + (7.days)
    limit_items =  ExpendableItem.where(user_id: user.id).where(deadline_on: limit_seven_days)
    if limit_items != []
      names = limit_items.map {|item| item.name } 
      response = "1週間以内に以下の消耗品が無くなります。\n早めの買い足しをオススメします。\n\n#{names.join("\n")}"
    else
      response = "1週間以内に無くなる消耗品はありません。"
    end
  end

  def shopping_list(response) ##メッセージの形式を作成
    {
      type: 'flex',
      altText: 'お買い物リスト',
      contents: {
        type: 'bubble',
        header:{
          type: 'box',
          layout: 'horizontal',
          contents:[
            {
              type: 'text',
              text: 'お買い物リスト',
              wrap: true,
              size: 'md',
            }
          ]
        },
        body: {
          type: 'box',
          layout: 'horizontal',
          contents: [
            {
              type: 'text',
              text: response,
              wrap: true,
              size: 'sm',
            }
          ]
        }
      }
    }
  end

  ...省略
end
```

#### 上記のコード内容について説明していきます。
```ruby
    body = request.body.read

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

    events = client.parse_events_from(body)

    events.each { |event| #②
      user_id = event['source']['userId']
      user = User.where(uid: user_id)[0]
      if event.message['text'].include?("お買い物リスト")
        message = shopping_list(send_limit_item(user))
      elsif event.message['text'].include?("自動購入機能テスト")
        message = test_selenium(user)
      end

      case event #③
      # メッセージが送信された場合
      when Line::Bot::Event::Message
        case event.type
        # メッセージが送られて来た場合
        when Line::Bot::Event::MessageType::Text
          client.reply_message(event['replyToken'], message)
        end
      end
    }

    head :ok
  end
```

核になるのは上記の部分となります。
- ①の部分では、400エラーを補足しています。
- ②の部分では実際にユーザから送られてきたメッセージを格納しているeventという変数を利用して処理の分岐を行っています。
    - `user_id = event['source']['userId']`でメッセージを送ってきたユーザの`uid`を特定し、`user = User.where(uid: user_id)[0]`でそのユーザをuserに格納しています。この記述を使用することで、他のcontrollerで`User.find`してきたのと同じようにユーザ情報を扱えるようにしています。
    - `event.message['text'].include?("お買い物リスト")`で送信されたメッセージに`お買い物リスト`が入っているかを確認して処理をしています。
- ③の部分で、リプライの処理を行っています。

また、メッセージの内容と形式についてはプッシュメッセージでも使用するので、私はモジュールに切り出しを行い再利用性を高めております。

### メッセージの固定化とリッチメニューの作成
ここからは少々おまけ部分で、ユーザが送信してくるメッセージの固定化を行います。
この設定をする目的は、ユーザが文字を打つことを減らしUXの向上と、上記の処理の中でif文で処理を分岐させているので確実にその文字を入力させるように誘導することで処理の成功率を上げるためです。

まずはこちらにアクセスをしてください。
https://manager.line.biz/

そうしますと、自身のチャネルが表示されるので利用しているチャネルを選択します。
画面が切り替わり、自身のチャネルのホームになるので、左側のメニューからリッチメニューを選択します。
![スクリーンショット 2023-01-14 23.31.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/2dec1be2-d2e9-0c30-e40c-299c84c8782b.png)
上記のボタンから作成を選択します。
そうすると以下の画面になるので、必要項目を入力していきます。
![FireShot Capture 002 - LINE Official Account Manager - manager.line.biz.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/6631ffba-ca38-1c04-b27d-070200258a8d.png)

特にコンテンツの設定で実際に使用するメニューを作成していくので気合を入れましょう。
テンプレートの選択をした後、メニューの画像を作成していきます。
実際にどのように表示されるかプレビューを確認できるので確認しながら作成していきます。

![スクリーンショット 2023-01-14 23.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881858/5dae35c7-57d9-e903-c1d3-e88c51f4978b.png)
アクションの設定では、タイプを`テキスト`にしキーワードの中に実際にボタンを押した際に送信されるメッセージを入力します。
ここで入力するメッセージはlinebot_controller.rb内のcallbackメソッドのif文の中で指定するメッセージと同一のものにしておきます。

これで、リッチメニューの作成とメッセージの固定化の完了です。
保存して反映されているか確認しましょう。(注:リッチメニューはスマホでのみ表示されます。PCやタブレットでは表示されません。)

ここまで、設定が完了するとユーザからのメッセージに対してリプライを送れるようになります。


## テストについて

ここまで、LINEログインとプッシュ通知について実装をしてきましたが、ここからはきちんと実装できたかのテストをしていきます。

今回はRSpecを使用したテストを行なっていきます。

### RSpecの導入
下記を記載し、`bundle install`を実行します。
```ruby:Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 5.0.0'
  gem 'rexml'
  gem 'factory_bot_rails'
end
```
下記コマンドでRSpecをインストールします。
```terminal:ターミナル
rails generate rspec:install
```

### LINEログインのテスト
ここまで実行するとspecというフォルダがRails直下に作成され、その中に`rails_helper.rb`が作成されると思います。
23行目あたりにある以下のコードのコメントアウトを外してください。
```ruby:spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
```
次に、specフォルダ内にsupportというフォルダを作成します。
その中にomniauth.rbというファイルを作成してください。
omniauth.rb内には以下を記述します。

```ruby:spec/support/omniauth.rb
# テスト用にモックを使うための設定
# '/auth/provider'へのリクエストが、即座に'/auth/provider/callback'にリダイレクトされる
OmniAuth.config.test_mode = true

# Line用のモック
# '/auth/provider/callback'にリダイレクトされた時に渡されるデータを生成
OmniAuth.config.mock_auth[:line] = OmniAuth::AuthHash.new(
  provider: 'line', 
  uid: '12345', 
  info: { email: 'test1@example.com', name: 'test_user' },
  credentials: { token: '1234qwer' }
)
```

上記のコードはLINEログインのmockを作成しています。
mockというのは外部APIとの連携をとるシステムのテストの際に、外部と通信をするとテストの結果が外部に依存してしまうため仕様変更などに結果が左右されないようにするための偽物の結果を返すプログラムです。
今回は、LINEでログインしていないがLINEでログインしたかのように振る舞うプログラムを作成しています。

#### 実際にログインをテストするコードを書く
```ruby:spec/system/user_spec.rb
require 'rails_helper'

RSpec.describe "Users", type: :system do
  describe 'Login処理' do
    before do
      Rails.application.env_config["devise.mapping"] = Devise.mappings[:user]
      Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:line]
    end
    context 'LINEログインボタンを押した場合' do
      it 'ログインができる' do
        visit new_user_session_path
        click_on  'line_login'
        expect(page).to have_content 'お買い物リスト'
      end
    end
  end
end
```

上記のコードの`before do`内で先ほどのmockを呼び出し、ログインボタンが押された際にすぐに処理を奪っています。


### プッシュ通知のテストについて
今回、私はプッシュ通知のテストについては非同期処理の部分のテストのみにしています。
理由としては、mockを使用したテストの場合実際に通知がきちんと送られているかをスクレイピングで判断することができないためです。

## まとめ
今回はかなり長くなりましたが、ポートフォリオに実装したLINE機能について実装からテストまでまとめてみました。
もし、間違っている部分やこれより良い方法などありましたらコメントをいただけると嬉しいです！

## 参考文献
https://qiita.com/s10aim_tana/items/2d174d4e31e4041700ee
https://qiita.com/kaiyukun/items/df8234634d2acc1be0f7
https://qiita.com/manbolila/items/5b6636ad64a5b4b7b074
https://qiita.com/jnchito/items/640f17e124ab263a54dd
https://qiita.com/tsukakei/items/82a2f8ddaddad1530dda
https://github.com/line/line-bot-sdk-ruby
https://qiita.com/clubysg/items/70309673376fd4cc7b2b
https://note.com/wktq/n/nb5ffe9eaddae