Rubyで作るLINEメッセージング機能の作り方

## はじめに

Rubyで書かれたシステムと一般ユーザのLINE間でメッセージのやり取りをする機能を開発する機会があったので、その時に得た知見を共有します。

また実際に動くアプリケーションのデモをRails使って作ったので、詳しい実装の詳細は [こちら](https://github.com/tkm-kj/line-api-sample/tree/v1.0) を見て頂くと、スムーズに記事の内容が入ってくると思います。

## 作るもの

システム管理者(以下「管理者」)と一般ユーザ（以下「ユーザ」）でLINEを媒介としたテキストメッセージのやり取り機能を作ります。
実際にやり取りをするためにユーザのLINEの情報をシステムが知っている必要があるので、ブラウザ上でのLINEログインも実装します。

ユーザのLINEの情報を紐付けた後は、ユーザが管理者に紐づくLINE公式アカウントにメッセージを送ると、そのメッセージがWebhookでシステムに飛んでいき、そこからデータベースに保存されます。ざっくり以下の図のようなイメージです。
![Untitled Diagram (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/33b578b2-5a1a-a909-6eff-cbe306d8a0a7.png)

管理者もユーザに対してシステムを通してLINEのメッセージを送ることが出来ます。
システムからテキストを送信すると、プログラムを通して管理者のLINE公式アカウントから対象のユーザにメッセージを送れます。送れたらそのメッセージがDBに保存されます。イメージは以下になります。
![Untitled Diagram (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/4fc2fe00-359f-ca63-e569-42d557678757.png)

## 事前に必要な作業

### LINE APIのチャネルを作成する

管理者がLINE APIのチャネルを持っていないとユーザとLINEでやり取り出来ないので作成します。
[LINE Developer console](https://developers.line.biz/console) から作っていきましょう。
今回作る必要があるチャネルは

- Messaging API
- LINEログイン

の2つです。

Messaging APIはLINEの公式アカウントとして機能します。作成の注意点として、**応答モードは「Bot」** にしましょう。チャットモードだとLINE公式アカウントマネージャーからやり取りが出来るのですが、Webhookを受け取れなくなっちゃうんですよね。なので、システムにやりとりのログを残せないという問題があるためBotモードで使う必要があります。詳しい作成方法は [こちら](https://developers.line.biz/ja/docs/messaging-api/getting-started/#%E3%83%81%E3%83%A3%E3%83%8D%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90) をどうぞ。
LINEログインは文字通りLINEログインを使用するためのチャネルとなっています。

作成が終わったらDBのadminsテーブルにデータを作ります。本来は画面から登録できるのが良いと思いますが、横着してコンソールから直で突っ込む作りになっています（プロダクションコードでDBに入れる際はセキュリティを考慮して暗号化して入れておくのをおすすめします）

```ruby
  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](https://ngrok.com/) を使うことでローカルのURLを一時的に外部公開する事が出来るので、それで作られたURLを登録することでローカルでも快適に開発をすることが出来ます。とても便利ですね。

Webhook URL用のPOSTリクエストのパスを作ってコンソールに設定してあげたら、「検証」ボタンを通してリクエストの検証をします。200のレスポンスが返るようにします。詳しくは [こちら](https://developers.line.biz/ja/docs/messaging-api/building-bot/#%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E3%81%A7%E3%83%9C%E3%83%83%E3%83%88%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B) をどうぞ。

以上で開発するための下準備が整ったので、これからゴリゴリ開発をしていきたいと思います。

## ユーザのLINEアカウントをシステムに連携させる

最初にメッセージの送受信で必要なユーザのLINEの情報をDBに入れるためのLINEログイン機能を作っていきたいと思います。このLINEの情報というのは「ユーザーID」です。ID検索で使われるLINE IDではないので注意してください。

ログインのフローは[こちら](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E3%83%95%E3%83%AD%E3%83%BC)になりますが、改めて書いていくと

1. 必要なクエリパラメータを使用して、システムからLINEログインの認可URLにユーザを誘導する
2. LINEログイン画面がブラウザで開き、ユーザはログインして認証を受ける。
3. LINEプラットフォームからシステムに、 `redirect_uri` を介してユーザーをリダイレクトする。このとき、認可コードと `state` を含むクエリ文字列もシステムに送信される。
4. `state` を検証した後、 システムは認可コードを使ってエンドポイントURL https://api.line.me/oauth2/v2.1/token にアクセストークンを要求する。
5. LINEプラットフォームがシステムからのリクエストを検証し、アクセストークンを返す。
6. アクセストークンを元にSocial APIを呼び出してユーザーIDを取得してDBに格納する

1の工程の認可URLを作成するコードを見ていきます。 

```app/controllers/auth_controller.rb
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` に渡しています。その中のコードを見てみます。

```app/models/line/api/oauth.rb
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
```

パラメータの詳しい意味は [こちら](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request) を見てください。個人的に、 `bot_prompt` パラメータが便利でLINEログインをした後に対応してる公式アカウントの友達追加の画面も出してくれるので、メッセージのやり取りまで持っていきやすくなって便利だなーと思いました。

URLを叩くと下の画像のような形でログイン画面にリダイレクトされます。

![スクリーンショット 2020-06-16 1.52.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/97b33391-2421-96e9-1113-9037dbe993ca.png)

ログインが完了して認可を終えると、先程設定したLINEログインのコールバックURLにリダイレクトします。 `/admin/:admin_id/callback` に向けたので、対応するコードを見ていきます。

```app/controllers/callback_controller.rb
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取得のコードを見てみます。

```app/models/line/api/oauth.rb
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](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#get-access-token)を叩いて情報を返し、それを Social API の[IDトークン検証API](https://developers.line.biz/ja/reference/social-api/#verify-id-token)に投げて成功すると、ユーザー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のコントローラのコードは以下です。

```app/controllers/webhook_controller.rb
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プラットフォームからのリクエストか判定するために [署名の検証](https://developers.line.biz/ja/reference/messaging-api/#signature-validation) をしてあげる必要があります。若干複雑ではありますが、 [line-bot-sdk-ruby](https://github.com/line/line-bot-sdk-ruby) というgemでやってくれるので使っちゃいましょう。

検証が終わったら、イベントがどんなものか判定を行います。リクエストボディからイベント情報のパースもgemでやってくれるので使っちゃいます。
イベントの分岐はcase文でやっちゃうのがシンプルかなと思います。 `Line::Bot::Event::Message` がメッセージが来たときのイベントなので、その時にメッセージの保存の処理を入れます。そのコードが以下です。

```app/models/line/save_received_message.rb
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 オブジェクトの中身については [こちら](https://developers.line.biz/ja/reference/messaging-api/#wh-text) を参照してください。

最後に動作確認です。
<img width="375" alt="iOS_の画像.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/f161af69-d1e4-c0b9-9e8f-4601413964d7.png">
メッセージを送ると

![スクリーンショット_2020-06-16_13_02_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/0ade574b-b13c-ee8d-f093-c1aa0a8f73f8.png)
リクエストが飛んでレコードが保存されていることがわかります。これでユーザのLINEメッセージをシステムで受け取ってDBに入れられるようになりました。

## システムからユーザにメッセージを送る

次は先程とは反対にシステム -> ユーザのLINEメッセージ送信を見ていきます。
こちらは、Webhookは使用する必要はなく [プッシュメッセージを送るAPI](https://developers.line.biz/ja/reference/messaging-api/#send-reply-message) を叩けば終わりです。

```app/models/line/save_sent_message.rb
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
```

```app/models/line/api/push.rb
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
```

動作確認です。横着してコンソールからコードを叩きます。

![スクリーンショット_2020-06-18_8_13_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/e1e55a1f-c292-0770-5d85-1ae5e55a20d4.png)
レコードが追加されました。

<img width="375" alt="iOS_の画像__1_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33557/ef8bc364-2d19-983e-1783-57491d851ffd.png">

LINEにメッセージが送られました！

## さいごに

以上で、ユーザとシステム間で相互のやり取りが出来てかつ、その内容を残すことが出来るようになりました。
実際のシステムではもっと複雑になると思いますが、これが何か実装の参考になればと思います :pray: