LoginSignup
50
43

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-04

はじめに

Lineログイン機能について

Railsアプリの作成

まずはじめに アプリを作成していない方はrails newしてアプリを作成してください。

Deviseの導入

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

Gemfile
gem 'devise'

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

次に、ターミナルで以下を実行し、Deviseを使えるようにしていきます。

ターミナル
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
無事にログインができると上記のような画面に移りますので、プロバイダーを作成します。
プロバイダー名はややこしくなるといけないので、自分のアプリの名前にするとわかりやすいです。
スクリーンショット 2023-01-04 13.05.29.png
作成が完了するとこのような画面になりますので、LINEログインを選択します。
新規チャネル作成画面になりますので、必須項目を記載し、作成ボタンをクリックします。

LINEログイン機能の導入

Gemfileに以下を記載し、bundle installを実行。

Gemfile
gem 'omniauth-line'
gem 'omniauth-rails_csrf_protection'

また、環境変数を設定するため下記のgemも追加します。

Gemfile
gem 'dotenv-rails'

上記のgemを追加したら、アプリのルート直下に.envファイルを作成し、以下の2つを追加します。

.env
LINE_KEY='自身のChannel ID'
LINE_SECRET='自身のChannel Secret'

スクリーンショット 2023-01-04 13.14.28.png
スクリーンショット 2023-01-04 13.17.32.png
次に、.envファイルをGitの管理から外すために.gitignoreに以下を追加。

.gitignore
/.env

DeviseとLINEログインの連携

Deviseの設定ファイルにLINEログインの設定を行います。
devise.rbに以下を追加

config/initializers/devise.rb
config.omniauth :line, ENV['LINE_KEY'], ENV['LINE_SECRET']

User.rbに以下を追加

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の作成

ターミナル
rails g controller omniauth_callbacks

omniauth_collbacks_controller内を以下の記述に変更

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

ルーティングの設定

ルーティングの設定を以下のようにします。

confing/routes.rb
devise_for :users, controllers: {
  omniauth_callbacks: "omniauth_callbacks"
}

モデルの変更

以下のコマンドでUserモデルにカラムを追加作成し、LINEログインができるようにします。

ターミナル
rails g migration add_column_to_users
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モデルに以下を追加

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
ただし、開発環境で実行する場合にhttp://localhost:3000/users/auth/line/callback のように設定して使うことはできません。
これはlocalhostは外部からアクセスすることができないからです。
外部からのアクセスを許可するためにngrokを使用していきます。

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

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

ターミナル
ngrok http 3000

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

【Rails6の場合】
Rails6の場合は以下を追記してngrokのアクセスを許可してください。

config/enviroments/development.rb
Rails.application.configure do
  config.hosts.clear
end

チャネルの公開

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

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

LINEでプッシュ通知を送る

LINE DevelopersのLINEログインを作成したプロバイダーにてMessaging APIを作成します。

画面の指示に従って、必須事項を入力していき作成ボタンをクリックして作成します。
無事に作成できたらMessaging API設定タブの一番下にあるチャンネルアクセストークンを発行しておきます。

Rails側の設定

以下を記載し、bundle installをします。

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
スクリーンショット 2023-01-04 14.20.07.png

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

LINEでメッセージを送信する

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

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

LINEDevelopersの設定

まず、LINEDevelopersにてwebhookの設定を行います。
スクリーンショット 2023-01-14 22.55.11.png
上記のように、MessagingAPI設定の画面にてWebhookの設定を行います。
Webhook URLに自身のアプリのURL/callbackを設定し、Webhookの利用をONにしてください。
(開発環境でテストを行う際にはLINEログインの際と同じようにngrokのURLを設定してください。)
その下の再送やエラーについての項目については必要に応じて設定を変えてください。

Rails側のroutesの設定

次に、Webhookから送られてきたリクエストを処理する際に、どこで処理するかをroutesで記載していきます。

config/routes.rb
Rails.application.routes.draw do
    post '/callback' => 'linebot#callback'
end

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

LinebotControllerの設定

最後に処理の要となるLinebotControllerを作成していきます。
まず、Controllerを作成するので下記コマンドを叩いてください。

ターミナル
rails g controller Linebot

そうしますと、contoroller以外にもviewなども作成されますが、viewについては今回は使用しないので削除してください。

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

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

上記のコード内容について説明していきます。

    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
上記のボタンから作成を選択します。
そうすると以下の画面になるので、必要項目を入力していきます。
FireShot Capture 002 - LINE Official Account Manager - manager.line.biz.png

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

スクリーンショット 2023-01-14 23.37.17.png
アクションの設定では、タイプをテキストにしキーワードの中に実際にボタンを押した際に送信されるメッセージを入力します。
ここで入力するメッセージはlinebot_controller.rb内のcallbackメソッドのif文の中で指定するメッセージと同一のものにしておきます。

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

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

テストについて

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

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

RSpecの導入

下記を記載し、bundle installを実行します。

Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 5.0.0'
  gem 'rexml'
  gem 'factory_bot_rails'
end

下記コマンドでRSpecをインストールします。

ターミナル
rails generate rspec:install

LINEログインのテスト

ここまで実行するとspecというフォルダがRails直下に作成され、その中にrails_helper.rbが作成されると思います。
23行目あたりにある以下のコードのコメントアウトを外してください。

spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

次に、specフォルダ内にsupportというフォルダを作成します。
その中にomniauth.rbというファイルを作成してください。
omniauth.rb内には以下を記述します。

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でログインしたかのように振る舞うプログラムを作成しています。

実際にログインをテストするコードを書く

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

50
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
43