はじめに
- 今回は【個人開発】日用品を自動で購入してくれるアプリを作りました。で実装したLINEログインとLINEbotについての解説記事になります。
- 連載企画の続編「RubyonRailsにスクレイピング機能を持たせてみる。」を書きました!
Lineログイン機能について
Railsアプリの作成
まずはじめに アプリを作成していない方はrails new
してアプリを作成してください。
Deviseの導入
LINEログイン機能を実装する前にまずはDeviseでログイン機能を実装していきます。
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/ にアクセスし、右上のログインからビジネスアカウントでログインを選びアカウントを作成してください。
無事にログインができると上記のような画面に移りますので、プロバイダーを作成します。
プロバイダー名はややこしくなるといけないので、自分のアプリの名前にするとわかりやすいです。
作成が完了するとこのような画面になりますので、LINEログインを選択します。
新規チャネル作成画面になりますので、必須項目を記載し、作成ボタンをクリックします。
LINEログイン機能の導入
Gemfileに以下を記載し、bundle install
を実行。
gem 'omniauth-line'
gem 'omniauth-rails_csrf_protection'
また、環境変数を設定するため下記のgemも追加します。
gem 'dotenv-rails'
上記のgemを追加したら、アプリのルート直下に.env
ファイルを作成し、以下の2つを追加します。
LINE_KEY='自身のChannel ID'
LINE_SECRET='自身のChannel Secret'
次に、.env
ファイルをGitの管理から外すために.gitignore
に以下を追加。
/.env
DeviseとLINEログインの連携
Deviseの設定ファイルにLINEログインの設定を行います。
devise.rb
に以下を追加
config.omniauth :line, ENV['LINE_KEY'], ENV['LINE_SECRET']
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内を以下の記述に変更
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
ルーティングの設定
ルーティングの設定を以下のようにします。
devise_for :users, controllers: {
omniauth_callbacks: "omniauth_callbacks"
}
モデルの変更
以下のコマンドでUserモデルにカラムを追加作成し、LINEログインができるようにします。
rails g migration add_column_to_users
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モデルに以下を追加
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ログインができるようになります。
ただし、開発環境で実行する場合にhttp://localhost:3000/users/auth/line/callback のように設定して使うことはできません。
これはlocalhostは外部からアクセスすることができないからです。
外部からのアクセスを許可するためにngrokを使用していきます。
開発環境で外部からのアクセスを許可する
上記サイトにアクセスし、sign upしてngrokをインストールしてください。
set up & installationの項目が全て完了したら下記コマンドが打てるようになるはずです。
ngrok http 3000
また、Railsサーバーも起動しないといけないのでターミナルを分割してrails s
でサーバーを起動してください。
【Rails6の場合】
Rails6の場合は以下を追記してngrokのアクセスを許可してください。
Rails.application.configure do
config.hosts.clear
end
チャネルの公開
最後に作成したチャネルを公開します。
上にある非公開ボタンをクリックし、チャネルを公開にしてください。
チャネルが公開に切り替わればLINEログインができるようになります。
LINEでプッシュ通知を送る
LINE DevelopersのLINEログインを作成したプロバイダーにてMessaging API
を作成します。
画面の指示に従って、必須事項を入力していき作成ボタンをクリックして作成します。
無事に作成できたらMessaging API設定タブの一番下にあるチャンネルアクセストークンを発行しておきます。
Rails側の設定
以下を記載し、bundle install
をします。
gem 'line-bot-api'
LINEログインの際に作成した.env
に以下を追記します。
LINE_CHANNEL_SECRET='channel Secret'
LINE_CHANNEL_TOKEN='Channel access token'
ここまででbotの設定編は終了です。
LINEでメッセージを送信する
ここではActiveJobを用いてメッセージを送信していきます。
ActiveJob以外でもcontroller内に記載してやることでcreateやupdateアクション時にメッセージを同期的に送ることも可能です。
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に対して送信された際、それを起点としてプログラムを走らせる処理を実装していきます。
完成系は下記となります。
LINEDevelopersの設定
まず、LINEDevelopersにてwebhookの設定を行います。
上記のように、MessagingAPI設定の画面にてWebhookの設定を行います。
Webhook URLに自身のアプリのURL/callback
を設定し、Webhookの利用をONにしてください。
(開発環境でテストを行う際にはLINEログインの際と同じようにngrokのURLを設定してください。)
その下の再送やエラーについての項目については必要に応じて設定を変えてください。
Rails側のroutesの設定
次に、Webhookから送られてきたリクエストを処理する際に、どこで処理するかをroutesで記載していきます。
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内の記述をしていきます。
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/
そうしますと、自身のチャネルが表示されるので利用しているチャネルを選択します。
画面が切り替わり、自身のチャネルのホームになるので、左側のメニューからリッチメニューを選択します。
上記のボタンから作成を選択します。
そうすると以下の画面になるので、必要項目を入力していきます。
特にコンテンツの設定で実際に使用するメニューを作成していくので気合を入れましょう。
テンプレートの選択をした後、メニューの画像を作成していきます。
実際にどのように表示されるかプレビューを確認できるので確認しながら作成していきます。
アクションの設定では、タイプをテキスト
にしキーワードの中に実際にボタンを押した際に送信されるメッセージを入力します。
ここで入力するメッセージはlinebot_controller.rb内のcallbackメソッドのif文の中で指定するメッセージと同一のものにしておきます。
これで、リッチメニューの作成とメッセージの固定化の完了です。
保存して反映されているか確認しましょう。(注:リッチメニューはスマホでのみ表示されます。PCやタブレットでは表示されません。)
ここまで、設定が完了するとユーザからのメッセージに対してリプライを送れるようになります。
テストについて
ここまで、LINEログインとプッシュ通知について実装をしてきましたが、ここからはきちんと実装できたかのテストをしていきます。
今回はRSpecを使用したテストを行なっていきます。
RSpecの導入
下記を記載し、bundle install
を実行します。
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行目あたりにある以下のコードのコメントアウトを外してください。
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
次に、specフォルダ内にsupportというフォルダを作成します。
その中にomniauth.rbというファイルを作成してください。
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でログインしたかのように振る舞うプログラムを作成しています。
実際にログインをテストするコードを書く
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