はじめに
Rubyで書かれたシステムと一般ユーザのLINE間でメッセージのやり取りをする機能を開発する機会があったので、その時に得た知見を共有します。
また実際に動くアプリケーションのデモをRails使って作ったので、詳しい実装の詳細は こちら を見て頂くと、スムーズに記事の内容が入ってくると思います。
作るもの
システム管理者(以下「管理者」)と一般ユーザ(以下「ユーザ」)でLINEを媒介としたテキストメッセージのやり取り機能を作ります。
実際にやり取りをするためにユーザのLINEの情報をシステムが知っている必要があるので、ブラウザ上でのLINEログインも実装します。
ユーザのLINEの情報を紐付けた後は、ユーザが管理者に紐づくLINE公式アカウントにメッセージを送ると、そのメッセージがWebhookでシステムに飛んでいき、そこからデータベースに保存されます。ざっくり以下の図のようなイメージです。
管理者もユーザに対してシステムを通してLINEのメッセージを送ることが出来ます。
システムからテキストを送信すると、プログラムを通して管理者のLINE公式アカウントから対象のユーザにメッセージを送れます。送れたらそのメッセージがDBに保存されます。イメージは以下になります。
事前に必要な作業
LINE APIのチャネルを作成する
管理者がLINE APIのチャネルを持っていないとユーザとLINEでやり取り出来ないので作成します。
LINE Developer console から作っていきましょう。
今回作る必要があるチャネルは
- Messaging API
- LINEログイン
の2つです。
Messaging APIはLINEの公式アカウントとして機能します。作成の注意点として、応答モードは「Bot」 にしましょう。チャットモードだとLINE公式アカウントマネージャーからやり取りが出来るのですが、Webhookを受け取れなくなっちゃうんですよね。なので、システムにやりとりのログを残せないという問題があるためBotモードで使う必要があります。詳しい作成方法は こちら をどうぞ。
LINEログインは文字通りLINEログインを使用するためのチャネルとなっています。
作成が終わったらDBのadminsテーブルにデータを作ります。本来は画面から登録できるのが良いと思いますが、横着してコンソールから直で突っ込む作りになっています(プロダクションコードでDBに入れる際はセキュリティを考慮して暗号化して入れておくのをおすすめします)
create_table "admins", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.integer "line_messaging_id", null: false # Messaging APIの「チャネルID」
t.string "line_messaging_secret", null: false # Messaging APIの「チャネルシークレット」
t.string "line_messaging_token", null: false # Messaging APIの「チャネルアクセストークン」
t.integer "line_login_id", null: false # LINE Loginの「チャネルID」
t.string "line_login_secret", null: false # LINE Loginの「チャネルシークレット」
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
各種URLの設定をする
今度はいくつかURLをLINEのコンソール側に設定してあげる必要があるので、登録していきます。具体的には以下です。
- Messaging APIの Webhook URL(ユーザがメッセージを送った時にシステム側で受けるURL)
- LINEログインのコールバックURL(ログインした後のリダイレクトURL)
このURLを登録する際に気を付けなければいけないことがあります。それは、URLが
- https限定
- localhost使用不可
という点です。このままだとローカルでの開発が困難に思えます。
しかし、 ngrok を使うことでローカルのURLを一時的に外部公開する事が出来るので、それで作られたURLを登録することでローカルでも快適に開発をすることが出来ます。とても便利ですね。
Webhook URL用のPOSTリクエストのパスを作ってコンソールに設定してあげたら、「検証」ボタンを通してリクエストの検証をします。200のレスポンスが返るようにします。詳しくは こちら をどうぞ。
以上で開発するための下準備が整ったので、これからゴリゴリ開発をしていきたいと思います。
ユーザのLINEアカウントをシステムに連携させる
最初にメッセージの送受信で必要なユーザのLINEの情報をDBに入れるためのLINEログイン機能を作っていきたいと思います。このLINEの情報というのは「ユーザーID」です。ID検索で使われるLINE IDではないので注意してください。
ログインのフローはこちらになりますが、改めて書いていくと
- 必要なクエリパラメータを使用して、システムからLINEログインの認可URLにユーザを誘導する
- LINEログイン画面がブラウザで開き、ユーザはログインして認証を受ける。
- LINEプラットフォームからシステムに、
redirect_uri
を介してユーザーをリダイレクトする。このとき、認可コードとstate
を含むクエリ文字列もシステムに送信される。 -
state
を検証した後、 システムは認可コードを使ってエンドポイントURL https://api.line.me/oauth2/v2.1/token にアクセストークンを要求する。 - LINEプラットフォームがシステムからのリクエストを検証し、アクセストークンを返す。
- アクセストークンを元にSocial APIを呼び出してユーザーIDを取得してDBに格納する
1の工程の認可URLを作成するコードを見ていきます。
class AuthController < ApplicationController
def index
admin = Admin.find(params[:admin_id])
state = SecureRandom.hex(32)
session[:state] = state
redirect_to Line::Api::Oauth.new(admin).auth_uri(state)
end
end
/admin/:admin_id/auth で対象の管理者のLINEログインのアカウント情報を元に認可URLにリダイレクトさせます。
stateはCSRF対策で必要なので、アプリ側でランダムな数を生成します。その値はsessionに格納する(認可後の処理で使います)のと Line::Api::Oauth#auth_uri
に渡しています。その中のコードを見てみます。
AUTH_URI = 'https://access.line.me/oauth2/v2.1/authorize'
def auth_uri(state)
params = {
response_type: 'code',
client_id: @admin.line_login_id,
redirect_uri: callback_uri,
state: state,
scope: 'openid',
prompt: 'consent', # 必ずLINE認証を許可するようにするオプション
bot_prompt: 'aggressive' # ログイン後に連携した公式アカウントと友だちになるか聞く画面を出してくれる
}
# NOTE: https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request
"#{AUTH_URI}?#{params.to_query}"
end
パラメータの詳しい意味は こちら を見てください。個人的に、 bot_prompt
パラメータが便利でLINEログインをした後に対応してる公式アカウントの友達追加の画面も出してくれるので、メッセージのやり取りまで持っていきやすくなって便利だなーと思いました。
URLを叩くと下の画像のような形でログイン画面にリダイレクトされます。
ログインが完了して認可を終えると、先程設定したLINEログインのコールバックURLにリダイレクトします。 /admin/:admin_id/callback
に向けたので、対応するコードを見ていきます。
class CallbackController < ApplicationController
def index
admin = Admin.find(params[:admin_id])
# stateが異なっていたら例外を出す
raise Line::InvalidState unless params[:state] == session[:state]
line_user_id = Line::Api::Oauth.new(admin).line_user_id(params[:code])
User.create!(line_user_id: line_user_id)
render plain: 'LINE連携完了!', status: :ok
end
end
認可後に来る state と以前入れたsessionのstateの値を比較してCSRF対策をしています。その後、LINEのユーザーIDを取得します。取得したらそれをDBに格納します。ユーザーID取得のコードを見てみます。
def line_user_id(code)
verify_id_token(access_token_data(code))['sub']
end
def access_token_data(code)
req_body = {
grant_type: 'authorization_code',
code: code,
redirect_uri: callback_uri, # NOTE: LINEログインのチャネルのコンソールで設定した「コールバックURL」と比較している。
client_id: @admin.line_login_id,
client_secret: @admin.line_login_secret
}
# NOTE: https://developers.line.biz/ja/docs/line-login/integrate-line-login/#get-access-token
res = conn.post do |req|
req.url '/oauth2/v2.1/token'
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
req.body = req_body
end
JSON.parse(handle_error(res).body)
end
def verify_id_token(access_token_data)
req_body = {
id_token: access_token_data['id_token'],
client_id: @admin.line_login_id
}
# NOTE: https://developers.line.biz/ja/reference/social-api/#verify-id-token
res = conn.post do |req|
req.url '/oauth2/v2.1/verify'
req.body = req_body
end
JSON.parse(handle_error(res).body)
end
access_token_data
でアクセストークン取得APIを叩いて情報を返し、それを Social API のIDトークン検証APIに投げて成功すると、ユーザーIDが手に入ります。
気をつけるべき点としては、アクセストークン取得APIで redirect_uri
を渡していますが、これは認可URIで設定した redirect_uri
と全く同じ値にする必要があります。あまりやることはないかもしれませんが、諸事情で認可URL作成時の redirect_uri
にクエリパラメータで何か渡してコールバックでそれを取得したい場合、そのクエリパラメータも込みでアクセストークン取得APIの redirect_uri
に渡さないとエラーになります。
ユーザのメッセージをシステムで受け取る
ユーザが管理者のLINE公式アカウントと連携することが出来たので、次にユーザが送ったLINEメッセージをシステムで受け取りたいと思います。
Messaging APIのWebhook URLに /admin/:admin_id/webhook
のパスのURLを指定したので、その公式アカウントに何らかのイベント(友達追加・ブロック・メッセージの送信etc)が起こるたびにリクエストが飛ぶようになります。
なので、そのリクエストから メッセージが送信されたイベント && それを送った人がDB上に保存されているLINEユーザーID だったら、そのメッセージをDBに保存すると、ユーザからのLINEメッセージを集めることが出来ます。
webhookのコントローラのコードは以下です。
class WebhookController < ApplicationController
protect_from_forgery with: :null_session
def create
admin = Admin.find(params[:admin_id])
bot = Line::Api::Bot.new(admin)
body = request.body.read
raise Line::InvalidSignatureError unless bot.validate_signature?(body, request.env['HTTP_X_LINE_SIGNATURE'])
events = bot.parse_events_from(body)
events.each do |event|
case event
when Line::Bot::Event::Message
Line::SaveReceivedMessage.new(admin).call(event)
end
end
render plain: 'success!', status: :ok
end
end
外部からリクエストが飛んでくるので、CSRFトークンは無効にします。
また、LINEプラットフォームからのリクエストか判定するために 署名の検証 をしてあげる必要があります。若干複雑ではありますが、 line-bot-sdk-ruby というgemでやってくれるので使っちゃいましょう。
検証が終わったら、イベントがどんなものか判定を行います。リクエストボディからイベント情報のパースもgemでやってくれるので使っちゃいます。
イベントの分岐はcase文でやっちゃうのがシンプルかなと思います。 Line::Bot::Event::Message
がメッセージが来たときのイベントなので、その時にメッセージの保存の処理を入れます。そのコードが以下です。
module Line
class SaveReceivedMessage
def initialize(admin)
@admin = admin
end
def call(event)
user = User.find_by(line_user_id: event['source']['userId'])
resource = MessageText.new(content: event.message['text'])
Message.create!(sendable: user, receivable: @admin, resource: resource) if user.present?
end
end
end
event オブジェクトの中身については こちら を参照してください。
リクエストが飛んでレコードが保存されていることがわかります。これでユーザのLINEメッセージをシステムで受け取ってDBに入れられるようになりました。
システムからユーザにメッセージを送る
次は先程とは反対にシステム -> ユーザのLINEメッセージ送信を見ていきます。
こちらは、Webhookは使用する必要はなく プッシュメッセージを送るAPI を叩けば終わりです。
module Line
class SaveSentMessage
def initialize(admin)
@admin = admin
end
def call_with_text(user:, text:)
user = User.find_by(line_user_id: user.line_user_id)
if user.present?
Line::Api::Push.new(@admin).call_with_text(user: user, text: text)
resource = MessageText.new(content: text)
Message.create!(sendable: @admin, receivable: user, resource: resource)
end
end
end
end
module Line::Api
class Push < Base
def call_with_text(user:, text:)
call(user: user, resource: Message::Text.new([text]))
end
private
def call(user:, resource:)
req_body = {to: user.line_user_id}.merge(resource.request_body)
# NOTE: https://developers.line.biz/ja/reference/messaging-api/#send-push-message
res = conn.post do |req|
req.url '/v2/bot/message/push'
req.headers['Content-Type'] = 'application/json'
req.headers['Authorization'] = "Bearer #{@admin.line_messaging_token}"
req.body = req_body.to_json
end
handle_error(res)
end
end
end
動作確認です。横着してコンソールからコードを叩きます。
LINEにメッセージが送られました!
さいごに
以上で、ユーザとシステム間で相互のやり取りが出来てかつ、その内容を残すことが出来るようになりました。
実際のシステムではもっと複雑になると思いますが、これが何か実装の参考になればと思います