Ruby on Rails で作る! Clova スキル 〜LINE 連携もあるよ〜

LINEの Clova 開発室・開発1チームに所属している安田篤史 (@ayasuda) です。この記事はLINE Advent Calendar 2018の22日目の記事です。

2018年といえば、Clova Developer Center βがオープンし、誰でも Clova スキルを開発することができるようになりました。 また、LINE では Clova SDK 以外にも様々なプロダクトを提供しています。これらを組み合わせることで、興味深いサービスを簡単に作れるようになります。 本記事では、Ruby on Rails (以下 Rails) で作られた簡単な Web アプリを Clova のスキルに改造する方法と、 LINE の様々な API を組み合わせる方法についてステップバイステップで説明していきます。

この文書で「作るもの/作らないもの」と「説明すること/説明しないこと」

本記事では Rails の基礎については説明を割愛させていただきます。 Rails の導入及び基礎などについては Ruby on Rails Guides またはその 日本語訳 をご参照ください。

本記事では実装済みの Rails アプリに LINE API を組み込んでいきます。 説明に用いる Rails アプリとしてはそれっぽい、シンプルな ToDo リストアプリを用います。 認証フレームワークとして devise が設定済みで、また、UserListTask の 3 モデルが用意されています。 これらは User has many ListList has many Task という関係になっています。 ソースコードは こちら に用意してあります。 ただし、本記事では具体的なビジネスロジックの実装 (タスクの絞り込みにとして何をどこまで提要するかなど) には言及しませんので、あくまでもご参考までとして参照いただければと思います。

また、使用する API のいくつかでは HTTPS に対応していて外部からアクセス可能なサーバが必要となります。 あらかじめサーバを用意するか、 ngrok などをお使いください。

最初のRails x Clova スキル では、 Clova スキルの開発をステップごとに記しながら、既存の Rails アプリに組み込む方法を記していきます。 また、この章だけでも、Ruby on Rails で Clova アプリを作る方法が十分に紹介できるかと思います。 続くアカウントリンクを実装してユーザ毎にタスクを読み上げられるようにする では、Clova のアカウント連携機能を実装することで、既存の Rails アプリのユーザと Clova のユーザアカウントとを連携する方法について説明していきます。 これにより、例えば自分の ToDo だけを Clova から確認できるように Clova スキルを実装できるはずです。 Messaging API で メッセージを送ろう では Clova からのユーザ情報を元にして、そのユーザの LINE にメッセージを送る方法について説明していきます。 例えば、Clova に「雨レーダーの画像送って」と言った際に LINE に雨レーダーの画像を送りたい、みたいな機能を実現したい際にはご参考にしていただければと思います。

それでは、まずは開発者サイトへアクセスするところから、ご案内いたします。

準備: LINE Developers でアカウント/プロバイダーを作る

LINE の提供する各種 API を使用するには、まずは LINE Developers にて各種設定を行う必要があります。早速アクセスしてみましょう。 右上のログインボタンをよりログインができます。また、その際にご自身の LINE アカウントを使用することができます。

ログインに成功すると、「プロバイダー」の一覧画面に遷移します。「プロバイダー」とはアプリを提供する個人または組織のこと(LINEプラットフォーム用語集)で、ユーザにはこの「プロバイダー」単位で 各種機能を提供します。「プロバイダー」は幾つでも作成可能ですが、 「プロバイダー」をまたいで情報のやり取りはできない ので注意してください。 「プロバイダー」は「新規プロバイダー作成」ボタンから作成可能です。

「新規プロバイダー作成」ボタンをクリックするとプロバイダーの作成フォームへ移動します。まずはプロバイダー名を設定します。プロバイダー名は将来アプリを公開する際の提供元となります。 なお、後から変更は可能です。

確認画面で入力内容の確認ができます。問題なければ「作成」ボタンをクリックし、プロバイダーを作成します。

「プロバイダー」が無事作成されると、「チャネル」作成が促されます。また、作成済みのプロバイダーが左サイドバーのプロバイダーリストから確認できるはずです。

これで準備はおしまいです!

この後は、各種「チャネル」を作成しつつ具体的な各種 API をアプリケーションに組み込んでいきます。

Rails x Clova スキル

Clova Extensions Kit (以下 CEK) を使えば、スマートスピーカー向けのスキルを作成することができます。 CEKとは、Clova Extension を開発および配布する際に、必要なツールとインターフェースを提供するプラットーフォーム (用語集: Clova Extensions Kit(CEK))です。 この章では、 Rails アプリに Clova スキル向けのハンドラを実装することで、ユーザ連携をしながら Web でも音声でも操作できるアプリケーションを作っていきます。

LINE Developers でチャネルを作成し、スキル・サーバの設定をする

Clova スキルの開発は、チャネルの作成から始めます。 LINE Developers のプロバイダー一覧から「新規チャネル作成」にて、「Clovaスキル」を選んでもチャネル作成ができますし、 Clova Developer Center β からもチャネル作成ができます。 今回は、 Clova Developer Center β からチャネルを作成する方法をご紹介します。

ヘッダの「スキル設定」をクリックし、スキル一覧へ移動します。

スキル一覧の下にある「LINE Developers でスキルチャネルを新規作成」をクリックして、チャネルの作成へと移動します。

このフローでチャネルを作成する際には、最初に使用するプロバイダーを尋ねられますので、そこだけお気をつけください。

LINE Developers でチャネルを作成すると、Clova Developer Center β へ移動します。

Clova Developer Center β のスキル作成フォームが表示されますので、スキルの情報を適宜埋めていってください。「タイプ」「Extension ID」以外は後から変更可能です。

全ての項目を埋めたら、「作成」ボタンをクリックしてスキルを作成します。フォームはまだまだ続き、「サーバ設定」フォームへ移動しますがスキル自体は作成済みとなりますので、 このタイミングから対話モデルの設定などを始めることができます。

対話モデルを用意しよう

準備ができたら、早速、Clova のスキル開発を始めましょう。

早速プログラムを書き始めたいところですが、まずは対話モデルの定義が必要です。 対話モデルは、ユーザの音声入力を JSON に変換するデータモデルで、Clova Extensions Kit の対話モデル編集画面から編集を行うことができます。 対話モデルを作成することで、例えば「明日やるタスクを教えて」「今日はなんのタスクがある?」「昨日の完了済みタスクを全部読んで」と言った音声入力の意図を理解して、下記に示す JSON に変換してサーバに送ることができます。

{
  "intent": {
    "name": "ReadListIntent",
    "slots": {
      "datetime": {
        "name": "datetime",
        "value": "2018-12-22",
        "valueType": "DATE"
      }
    }
  }
}

対話モデルは、主にインテントとスロットを定義することで作ります。

インテントとは、 ユーザの発話意図 のことで、主にユーザが発話した動詞で区別されます。上記の例では、 「〇〇なタスクを教えて」や「〇〇なタスクは何?」といった発話を、 ReadListIntent (タスクリストの読み上げ) と定義しています。

スロットとは 発話から取得される情報 で、主に名詞で区別されます。 上記の例では「明日」や「明後日」といった日付や、「完了済み」といったタスクの状態がスロットに当たります。

プログラミング言語で例えると、インテントがメソッド/関数、スロットがパラメタ/引数のようなものです。

注意が必要なのは、 ユーザの発話そのもの インテントにもスロットにもできません。ですので、例えば「タスクを追加する」というようなスキルは少し難しいという点です。

対話モデルや Extension の設計方法・手順については、推奨方法などが Extensionのデザインガイドライン に記載されておりますので、ぜひご一読ください。

インテントとスロットが整理できたら、さっそく対話モデルを作っていきましょう。

対話モデルを作ろう

対話モデルを編集する画面へは、 Clova Developer Center β から移動できます。 先ずはヘッダの「スキル設定」をクリックし、スキルの一覧を表示します

次に、対話モデルを設定したいスキルの、「対話モデル」列にある「修正」リンクをクリックします。

すると、対話モデルを編集するための画面が開かれます。

新しいインテントを定義するには、左サイドバーの「+」ボタンをクリックします。

新規のカスタムインテントを作成するフォームが表示されるので、インテント名を入力し、作成ボタンをクリックします。

インテントが作成され、サンプル発話の定義フォームが表示されます。このフォームにサンプル発話とサンプル発話毎にスロットを定義していくことでインテントの定義ができます。

早速、サンプル発話リストにサンプル発話を入力し、右端の「+」リンクをクリックしましょう。

サンプル発話が登録され、スロットの設定フォームが表示されるはずです。

さっそく、スロットを追加したいところですが、まだスロットタイプを一つも定義していないので今はできません。ですので、スロットタイプを定義していきましょう。 スロットタイプには2種類あり、独自で定義する「カスタムスロットタイプ」と、定義済みの「ビルトインスロットタイプ」があります。

先ずはビルトインスロットを対話モデルに追加しましょう。左サイドバーの「ビルトインスロットタイプ」右側にある「+」アイコンをクリックします。

使用するビルトインスロットタイプを選ぶフォームがあるので、使用するスロットにチェックを入れ、右上にある「保存」ボタンをクリックします。

選んだスロットが対話モデルに登録されます。

次に、カスタムスロットを定義していきましょう。左サイドバーの「カスタムスロットタイプ」右側にある「+」アイコンをクリックします。

新規のカスタムスロットタイプを作成するフォームが表示されるので、スロットタイプ名を入力し、作成ボタンをクリックします。

スロットタイプが作成され、辞書の登録フォームが表示されます。このフォームに単語を登録していくことでスロットタイプの定義ができます。

早速、スロットタイプの辞書に代表語を入れていきましょう。代表語を入力し、右側の「+」をクリックします。

次に、同義語の入力フォームが出てくるので、同義語を入力していきます。入力が終わったら、画面右上の「保存」ボタンをクリックしてスロットタイプを保存します。 例えば、スクリーンショットのように、代表語を「赤」とし、同義語を「赤い,紅,レッド」とすると、音声認識によって「赤い〜」「紅の〜」「レッドの〜」などと解析された結果が、 CEK を通して「赤」というスロットで送信されます。

スロットタイプの定義ができたら、インテントと紐付けましょう。左サイドバーから先ほど定義したカスタムインテントをクリックします。

サンプル発話とスロットの設定フォームが表示されるので、サンプル発話のスロットにしたい部分をドラッグします。するとスロット名の登録フォームが表示されます。

スロット名を入力し、「+」ボタンをクリックすると、スロットが登録されます。

スロットが登録されると、スロットタイプの設定ができるようになるので、スロットタイプを指定します。

無事、スロットタイプができたら、画面右上の「保存」ボタンをクリックし、インテントの定義を保存してください。

ここまでできたら一度テストをしてみましょう。テスト前に対話モデルをビルドする必要があります。左サイドバーの上部にある「ビルド」ボタンをクリックし、対話モデルをビルドします。

ビルドには少し時間がかかります。

ビルドができたら左サイドバーの「テスト」をクリックしましょう。

テスト用のフォームが表示されるので、テストしたい発話を入力し、「テスト」ボタンをクリックします。

すると、入力した日本語が Clova によって解析され、JSON に変換された上でサーバに送信されます。現時点ではサーバを特に設定していないので、CEK内部でエラーとなるはずです。 注目していただきたいのは「解析されたインテント」と「解析されたスロット」です。 解析されたインテントには先ほど登録したカスタムインテント、スロットにも先ほど登録したスロットが入っているかと思います。 また、「JSONのサービスリクエスト」には、実際に Extension サーバに送られる JSON が表示されており、その内部の intent オブジェクトも、先ほど登録したカスタムインテントとスロットが入っているはずです。

このように、インテント・スロットを定義していくことで対話モデルを構築できます。

インテントごとのサンプル発話は表現がシンプルなものであれば10個程度、人が管理可能な量の辞書を持つカスタムスロットタイプを使う場合は30個程度が目安となります。

Web のフォーム以外からでもサンプル発話やスロットタイプは編集可能です。それぞれのフォームにて「ダウンロード」ボタンをクリックすることで現在の定義に基づく tsv ファイルがダウンロードできます。 また、「アップロード」をクリックすることで、自前の tsv ファイルを元にインテントのサンプル発話リストやスロットタイプの代表語/同義語を登録することもできます。

サーバサイドを実装しよう

対話モデルの構築が終わったら、いよいよ Rails アプリ側での実装に入ります。

残念なことに、2018年12月22日現在では CEK の ruby 向け SDK はありません。 とはいえ、Extension サーバ側は指定のエンドポイントで形の決まった JSON を受け取り、形の決まった JSON を返すだけで OK ですので、 特にライブラリがなくても比較的容易にスキルの実装は可能です。

CEK からのリクエストは概ね次の 4 種類に分類されます。

  • スキル起動時にリクエストされる LaunchRequest
  • ユーザが発話したさいにリクエストされる IntentRequest
  • スキルの終了時にリクエストされる SessionEndedRequest
  • オーディオコンテンツ (音楽など) の再生開始や終了などデバイスの状態が変化した時に呼び出される EventRequest

この 4 種類のリクエストは、全て リクエストメッセージ のデータ構造に沿ってリクエストが送られてきます。 また、CEK へレスポンスは1種類のみで、 レスポンスメッセージ のデータ構造に沿って返す必要があります。 JSON の各フィールドの詳細については上記リンクをご参照ください。 

そんなわけで、まずはこんなデータ構造を定義するのが良いでしょう。各種リクエストの JSON 文字列を (割と力技で) ruby のクラスに押し込める実装が以下の通りとなります。

# app/models/clova/request.rb

module  Clova
  class Request
    attr_accessor :context, :request, :session, :version

    class Context
      attr_accessor :audio_player, :system

      class AudioPlayer
        attr_accessor :offset_in_milliseconds, :player_activity, :stream, :total_in_milliseconds

        def initialize(json)
          self.offset_in_milliseconds = json&.[]("offsetInMilliseconds")
          self.player_activity = json&.[]("playerActivity")
          self.stream = json&.[]("stream")
          self.total_in_milliseconds = json&.[]("totalInMilliseconds")
        end
      end # AudioPlayer

      class System
        attr_accessor :application, :device, :user

        class Application
          attr_accessor :application_id

          def initialize(json)
            self.application_id = json&.[]("applicationId")
          end
        end # Application

        class Device
          attr_accessor :device_id, :display

          class Display
            attr_accessor :content_layer, :dpi, :orientation, :size

            class ContentLayer
              attr_accessor :width, :height

              def initialize(json)
                self.width = json&.[]("width")
                self.height = json&.[]("height")
              end
            end # ContentLayer

            def initialize(json)
              self.content_layer = ContentLayer.new(json&.[]("contentLayer"))
              self.dpi = json&.[]("dpi")
              self.orientation = json&.[]("orientation")
              self.size = json&.[]("size")
            end
          end # Display

          def initialize(json)
            self.device_id = json&.[]("deviceId")
            self.display = Display.new(json&.[]("display"))
          end
        end # Device

        class User
          attr_accessor :user_id, :access_token

          def initialize(json)
            self.user_id = json&.[]("userId")
            self.access_token = json&.[]("accessToken")
          end
        end # User

        def initialize(json)
          self.application = Application.new(json&.[]("application"))
          self.device = Device.new(json&.[]("device"))
          self.user = User.new(json&.[]("user"))
        end
      end # System

      def initialize(json)
        self.audio_player = AudioPlayer.new(json&.[]("audioPlayer"))
        self.system = System.new(json&.[]("System"))
      end
    end # Context

    class Request
      attr_accessor :type

      # attr for EventRequest
      attr_accessor :request_id, :timestamp, :event

      # attr for IntentRequest
      attr_accessor :intent

      class Event
        attr_accessor :namespace, :name, :payload

        def initialize(json)
          self.namespace = json&.[]("namespace")
          self.name = json&.[]("name")
          self.payload  = json&.[]("payload")
        end
      end # Event

      class Intent
        attr_accessor :name, :slots

        def initialize(json)
          self.name = json&.[]("name")
          self.slots = json&.[]("slots")
        end
      end # Intent

      def initialize(json)
        self.type = json&.[]("type")
        self.request_id = json&.[]("requestId")
        self.timestamp = json&.[]("timestamp")
        self.event = Event.new(json&.[]("event"))
        self.intent = Intent.new(json&.[]("intent"))
      end
    end # Request

    class Session
      attr_accessor :new, :session_attributes, :session_id, :user

      class User
        attr_accessor :user_id, :access_token

        def initialize(json)
          self.user_id = json&.[]("userId")
          self.access_token = json&.[]("accessToken")
        end
      end # User

      def initialize(json)
        self.new = json&.[]("new")
        self.session_attributes = json&.[]("sessionAttributes")
        self.session_id = json&.[]("sessionId")
        self.user = User.new(json&.[]("user"))
      end
    end # Session

    def initialize(json)
      self.context = Context.new(json&.[]("context"))
      self.request = Request.new(json&.[]("request"))
      self.session = Session.new(json&.[]("session"))
      self.version = json["version"]
    end

    def self.parse_request_from(request_str)
      self.new(JSON.parse(request_str))
    end
  end # Request
end

Ruby on Rails の自動読み込みに対応させるために、空の module 定義も追加しておきましょう。

# app/models/clova.rb

module Clova; end

次に、 CEK からのリクエストを受け取るエンドポイントを作ります。基本的に CEK からのリクエストは単一のエンドポイントでのみ受け取ります。 今回は、 /clova で受け取れるように実装してみましょう。まずは、下記のコマンドでコントローラの雛形を作ります。

$ ./bin/rails g controller Clova index --no-assets

作成されたコントローラで /clova を受け取れるように、ルーティングを変更します。CEK からのリクエストは POST で行われます。

# config/routes.rb

Rails.application.routes.draw do
- get 'clova/index'
+ post '/clova', to: 'clova#index'
end

そして、コントローラ内で先ほど作成した Clova::Request を初期化します。 また、このコントローラでは CEK からの POST を受け付けます。そのため、このメソッドのみ CSRF 対策を切ります。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  protect_from_forgery except: :index

  def index
    clova_request = Clova::Request.parse_request_from(request.body.read)
  end
end

後は、リクエストの種類と、もしも IntentRequest ならばどのインテントかで処理を分けていくのがベターです。処理の切り分けをしやすくするために、 Clova::Request を少しだけ改造します。 具体的には、リクエストの種別判定と、インテント名・イベント名を Clova::Request オブジェクトから取得しやすくしておきましょう。 (追加するコードのみを抜粋します)

# app/models/clova/request.rb

require 'forwardable'

module Clova
  class Request
    extend ::Forwardable

    def_delegators :@request, :event?, :intent?, :launch?, :session_ended?, :name, :slots, :payload

    class Request
      def event?
        self.type == "EventRequest"
      end

      def intent?
        self.type == "IntentRequest"
      end

      def launch?
        self.type == "LaunchRequest"
      end

      def session_ended?
        self.type == "SessionEndedRequest"
      end

      def name
        case
        when event? then "#{self.event.namespace}.#{self.event.name}"
        when intent? then self.intent&.name
        else ""
        end
      end

      def slots
        self.intent&.slots
      end

      def payload
        self.intent&.payload
      end
    end
  end
end

Clova::Request を改造したので、コントローラの実装はこんな感じにするのが良いでしょう。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  def index
    clova_request = Clova::Request.parse_request_from(request.body.read)
    case
    when clova_request.event?
      case clova_request.name
      when "AudioPlayer.PlayStarted"
        # noop
      else
        # noop
      end
    when clova_request.intent?
      case clova_request.name
      when "ReadlistsIntent"
        # noop
      else
        # noop
      end
    when clova_request.launch?
      # noop
    when clova_request.session_ended?
      # noop
    else
      # something wrong?
      # noop
    end
  end
end

実際には、特にハンドルしなくていいリクエストは全てまとめてしまいましょう。

次に、レスポンスを作って行きます。レスポンスとなる JSON を毎回作っても良いのですが、毎回同じようなオブジェクトを作るのは手間です。 そこでコントローラに補助的に使えるメソッドを定義しておくと便利でしょう。 また、この時にきちんと Clova::Response クラスを定義しても良いのですが、すぐに JSON に変換することを考えると、ヘルパーメソッド中で Hash を直接作成したほうが 簡単で良いかと思います。ひとまず、空のレスポンスを返す empty、 単一のメッセージを返して会話を終了する say、メッセージを返して会話を継続する ask あたりのメソッドを実装すれば十分だと思います。

CEK へのレスポンスは思った以上に複雑に 構成可能 (複雑にするメリットはありませんが・・・) です。 そのため、どこまでをヘルパーメソッドにして、どこからをビジネスロジック上で作成するかはスキル次第でしょう。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  # 中略
  private

  def empty()
    Hash.new.tap do |root|
      root[:version] = "1.0"
      root[:sessionAttributes] = {}
      root[:response] = Hash.new.tap do |response|
        response[:card] = {}
        response[:directives] = []
        response[:outputSpeech] = {}
      end
    end
  end

  def say(message)
    Hash.new.tap do |root|
      root[:version] = "1.0"
      root[:sessionAttributes] = {}
      root[:response] = Hash.new.tap do |response|
        response[:card] = {}
        response[:directives] = []
        response[:outputSpeech] = Hash.new.tap do |output_speech|
          output_speech[:type] = "SimpleSpeech"
          output_speech[:values] = Hash.new.tap do |value|
            value[:type] = "PlainText"
            value[:lang] = "ja"
            value[:value] = message
          end
        end
        response[:shouldEndSession] = true
      end
    end
  end

  def ask(message, session_attributes)
    Hash.new.tap do |root|
      root[:version] = "1.0"
      root[:sessionAttributes] = {}
      root[:response] = Hash.new.tap do |response|
        response[:card] = {}
        response[:directives] = []
        response[:outputSpeech] = Hash.new.tap do |output_speech|
          output_speech[:type] = "SimpleSpeech"
          output_speech[:values] = Hash.new.tap do |value|
            value[:type] = "PlainText"
            value[:lang] = "ja"
            value[:value] = message
          end
        end
        response[:shouldEndSession] = true
      end
    end
  end
end

さて、ここまでの実装を一度整理して、動かしてみましょう。コントローラのハンドラを次のように書き換えてください。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  def index
    clova_request = Clova::Request.parse_request_from(request.body.read)
    case
    when clova_request.intent?
      case clova_request.name
      when "ReadListsIntent"
        render json: say("本日は晴天なり")
      else
        render json: empty
      end
    else
      render json: empty
    end
  end

  private
 
  # 中略
end

サーバを動かし、動きを見てみましょう。自前のサーバにデプロイをするか、 ngrok を使うなどし、 HTTPS でリクエストを受けられるようにしておいてください。

サーバを動かしたら、 Clova Developer Center β へ移動し、スキル設定の一覧画面から、基本情報の「修正」をクリックします。

次に、「サーバー設定」をクリックし、サーバの設定フォームを開きます。

サーバ設定フォームにてサーバーの URL (エンドポイントも含みます) を入力し、保存ボタンをクリックして情報を保存してください。

次に対話モデルの編集フォームを開き、「テスト」をクリックしてテストをしてみましょう。

うまく行けばきちんとソースコードで設定した応答ができているはずです。 また、このタイミングから実際のデバイスでも確認可能です。外部公開はされていないので、ご自身かテスターのアカウントでのみとなりますが、実際のデバイスでスキルを起動して試してみてください。

うまくいかない場合は対話モデル編集フォームの「テスト」の下に、発話履歴が表示されるので、音声認識の結果との比較も可能です。 発話履歴のリンクをクリックすると、発話履歴画面へ移動します。

発話履歴画面で「ログ取得を開始する」をクリックすると、リアルタイムで Clova デバイスへ話しかけた結果が確認できます。 音声認識の結果や、日本語解析の結果など、ここで確認してみてください。

さて、現在の実装では 本当に CEK から /clova へのリクエストなのかがわかりません 。そこで、 リクエストメッセージを検証するに基づいて、 リクエストを検証するようにコードを修正します。

まずは下記のように公開鍵をダウンロードして config ディレクトリ以下に保存します。

$ wget https://clova-cek-requests.line.me/.well-known/signature-public-key.pem
$ mv ./signature-public-key.pem config/

次にコントローラに下記メソッドを実装し、リクエストを検証するように実装します。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  before_action :validate_request, only: :index

  private

  def validate_request
    return true if Rails.env.test?
    key = OpenSSL::PKey::RSA.new(Rails.root.join('config', 'signature-public-key.pem').read)
    signature = request.headers[:HTTP_SIGNATURECEK]
    unless signature
      logger.warn("Signature missing")
      render text: "Access denied", status: 403
      return false
    end
    unless key.verify('sha256', Base64.decode64(signature), request.body.read)
      logger.warn("$ignature verificatoin failed")
      render text: "Access denied", status: 403
      return false
    end
  end
end

ここまででサーバサイドの基礎部分が出来上がりました、あとは、具体的な各種リクエストのハンドラを実装していくだけです! ここまでのコミットは これ と これ と これ の3つです。

アカウントリンクを実装してユーザ毎にタスクを読み上げられるようにする

さて、今回のサンプルアプリでは ユーザ毎に タスクが管理されています。

そこで ユーザーアカウントを連携する を実装し、ユーザ毎に ToDo リストを読み上げられるようにしましょう!

さて、Clova とアカウント連携をするためには、アプリケーションに OAuth 2.0 のプロバイダーとしての機能を実装する必要があります。

Ruby on Rails では Doorkeepr を使うと簡単に OAuth 2 provider を実装できます。

Doorkeeper を設定する

Doorkeeper は Ruby の Web フレームワークである Rails もしくは Grape に OAuth 2.0 の provider としての機能を追加するライブラリです。

早速 Doorkeeper を導入しましょう。まずは Gemfile に依存を追加し、 bundler でインストールをします。

gem 'doorkeeper'
$ bundle

次に、rails の generator で設定ファイル及び関連テーブルなどをインストールします。

$ rails generate doorkeeper:install
$ rails generate doorkeeper:migration

さっそく、マイグレーションファイルを実行したいところですが、Devise を使っているので生成したトークンと User との関連づけをマイグレーションファイルに足しておきましょう。

add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id
add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id

マイグレーションファイルを変更したら、マイグレーションを実行し、テーブルを作成します。

モデルの次にルーティングも変更しましょう。 Doorkeeper は OAuth2 の各種リクエストに対応したルーティングを用意してくれます。 config/routes.rb を以下のように編集します。

# config/routes.rb

Rails.application.routes.draw do
  use_doorkeeper
  # 中略
end

設定ができたら、ルーティングが用意されたか下記コマンドで試してみましょう。

$ ./bin/rails routes | grep oauth
native_oauth_authorization    GET    /oauth/authorize/native(.:format)            doorkeeper/authorizations#show
oauth_authorization           GET    /oauth/authorize(.:format)                   doorkeeper/authorizations#new
                              DELETE /oauth/authorize(.:format)                   doorkeeper/authorizations#destroy
                              POST   /oauth/authorize(.:format)                   doorkeeper/authorizations#create
oauth_token                   POST   /oauth/token(.:format)                       doorkeeper/tokens#create
oauth_revoke                  POST   /oauth/revoke(.:format)                      doorkeeper/tokens#revoke
oauth_introspect              POST   /oauth/introspect(.:format)                  doorkeeper/tokens#introspect
oauth_applications            GET    /oauth/applications(.:format)                doorkeeper/applications#index
                              POST   /oauth/applications(.:format)                doorkeeper/applications#create
new_oauth_application         GET    /oauth/applications/new(.:format)            doorkeeper/applications#new
edit_oauth_application        GET    /oauth/applications/:id/edit(.:format)       doorkeeper/applications#edit
oauth_application             GET    /oauth/applications/:id(.:format)            doorkeeper/applications#show
                              PATCH  /oauth/applications/:id(.:format)            doorkeeper/applications#update
                              PUT    /oauth/applications/:id(.:format)            doorkeeper/applications#update
                              DELETE /oauth/applications/:id(.:format)            doorkeeper/applications#destroy
oauth_authorized_applications GET    /oauth/authorized_applications(.:format)     doorkeeper/authorized_applications#index
oauth_authorized_application  DELETE /oauth/authorized_applications/:id(.:format) doorkeeper/authorized_applications#destroy
oauth_token_info              GET    /oauth/token/info(.:format)                  doorkeeper/token_info#show

準備ができたので動きを試してみたい……・ところですが、まだ OAuth の client を作成していません。 また、今のままでは誰も OAuth client を追加できません。ですので、認証の設定を変更し認証ロジックなどを追加しましょう。

まずは OAuth の Authorization Request ( ここの(A) ) で、どのように認証を行うかを設定します。 これは、 config/initializers/doorkeeper.rb 内の resource_owner_authenticator ブロックの中で設定します。 このブロック内ではどのように認証をするかのロジックを記していきますが、アプリケーション実行コンテキスト内で動きますので、セッッションやモデル、ヘルパーメソッドなどにアクセス可能です。

と、いうわけで、認証に Devise を使っているのでこんな感じの設定になります。

# config/routes.rb

Doorkeeper.configure do
  # 中略
  resource_owner_authenticator do
    current_user || warden.authenticate!(scope: :user)
  end
  # 中略
end

また、期限切れや破棄されたトークンを削除するためのタスクが rake タスクとして用意されていますので、使用する場合には Rakefile に以下のコードを追加してください。

# Rakefile

Doorkeeper::Rake.load_tasks

これにより、以下のコマンドで不要なトークンを削除することができます。

$ ./bin/rails doorkeeper:db:cleanup

期限切れのトークンを再発行する設定も入れておいた方が便利でしょう。

# config/routes.rb
Doorkeeper.configure do
  # 中略
  use_refresh_token
  # 中略
end

さて、早速動きを試して見たいところですが、今のところ Client がありませんし、 Client を作成・更新・削除するための /oauth/applications にアクセスができません (403 エラーになるはずです!) そこで、まずは「管理者ユーザのみ OAuth アプリケーションを管理できる」ようにする設定します。config/initializers/doorkeeper.rb についぎの設定を追加します。

# config/initializers/doorkeeper.rb

Doorkeeper.configure do
  # 中略
  admin_authenticator do
    if current_user
      head :forbidden unless current_user.admin?
    else
      warden.authenticate!(scope: :user)
    end
  end
  # 中略
end

また、 User クラスにそのユーザが管理者かどうか判定するメソッドを追加しておきましょう。(この辺はアプリケーションによって異なってくるので、サンプルコードは適当です)

class User < ActiveRecord::Base
  # 中略
  def admin?
    self.id == 1
  end
  # 中略
end

ここまでできたら、サーバを起動して動きを見て見ましょう。 サーバを起動した後、上記の User.amdin? が真になるユーザでログインし、 /oauth/applications へアクセスします。

アプリケーション一覧画面が開くので「New Application」リンクをクリックし、フォームを表示します。

Name には好きな名前を、Redirect URI も今回はテスト用で、サーバを localhost で動かすので “urn:ietf:wg:oauth:2.0:oob” を入れます。 Credential のチェックは入れっぱなしにし、Scopes は空白にした上で “Submit” をクリックしてアプリケーションを登録します。

すると、Client ID 及び Client Secret がそれぞれ生成されます。

これらの値が適切に設定されているか、irb を使ってテストして見ましょう。事前に oauth2 gem をインストールしておいてください。

$ gem install oauth2

準備が終わったら irb を起動して下記のコードを順に打ちます。

require 'oauth2'

client_id     = '...' # 上記で生成された Application UID を入れます
client_secret = '...' # 上記で生成された Secret を入れます
redirect_uri  = '...' # 上記で指定した Callback urls の中から一つを、今回なら urn:ietf:wg:oauth:2.0:oob を入れます
site          = "http://localhost:3000" # サーバを、テスト用なので左の値を入れます

client = OAuth2::Client.new(client_id, client_secret, site: site)
uri = client.auth_code.authorize_url(redirect_uri: redirect_uri)
puts uri

puts uri で認証用の URI が表示されるはずなので、ブラウザで開いて見てください。 セッションがなければ、ログイン画面が開くはずです。ですので、普通にログインをします。

ログインに成功すると、アプリケーションの許諾画面が表示されるので「Authorize」をクリックします。

すると、Authorization Code が発行されます。この値を記録して irb に戻ります。

この Code を元にトークンを下記の ruby コードから取得します。

code = "..." # 上記フローで生成された Authorizatoin code を入力します
token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
token.token

無事に取得できたでしょうか? 取得ができたら設定は完了です。

Clova のアカウント連携を設定しよう

Rails 側の設定ができたら Clova 側の設定を行います。Clova Developers Centerβ の「基本情報」の修正リンクから「サーバー設定」を開きます。

まず、「アカウント連携の有無」を「はい」にします。

次に「ログインURL」にログインに使うURL、つまり Doorkeeper の用意したログイン用の URL を入力します。

「クライアント ID 」には、後ほど生成するアプリケーションの Application UID を入力します。

「プライバシーポリシーのURL(日本語)」には、後日生成するポリシーページのURLを入力します。審査前までにはこのページを用意しておいてください。

「リダイレクトURL」の URL をコピペしておきましょう。後ほどアプリケーションを Doorkeeper で登録する際に使用します。

「アクセストークンURI」には Doorkeeper が用意したトークン生成用の URI を使用します。具体的には /oaut/token です。 また、直下の「アクセストークン再発行URI(任意)」にも、同じ URI を入れておいてください。

「クライアントシークレット」も「クライアントID」と同様で、後ほど生成するアプリケーションの Secret を入力します。

最後に「クライアント資格情報の転送方式(任意)」を「HTTP Basic (Recommended)」に設定します。

サーバ情報を保存したら、先ほどのテスト時と同じように Doorkeper のアプリケーション管理画面を開き、 アプリケーションを登録しましょう。Callback url には、Clova の「リダイレクトURL」を使用します。

さて、ここまで設定できたら、一度アカウント連携を試して見ましょう。 自分の LINE ID で Clova を使っている場合は、スキルストアからテスト中のスキルが見えるようになっているはずです。

アカウント連携を追加したスキルをタップし、スキル詳細を開きます。 そして、右上の「利用開始」をタップするとアカウント連携が開始し、ログインフォームが開きます。

ログインフォームでは普通にメールアドレスとパスワードを入力してログインをします。

すると、アカウント連携が行われ、スキルストアへ戻ってきます。

これでアカウント連携ができました。

以降の CEK からのリクエストでは context.System.user に .userId だけではなく .accessToken として発行したトークンが送られてきます

これら2つを結びつけ、さらにアプリケーション側のユーザアカウントと結びつけることで、 特定のユーザに特化した Clova スキルを提供可能になります。

Rails の Clova コントローラーで、アカウント連携したユーザを取得しよう

さて、早速 Clova コントローラでユーザを取得できるようにしていきましょう。 発行したトークンは CEK のリクエストに含まれてくるため、Doorkeeper の組み込み認証処理を使うよりも 自前でユーザ取得を行なった方が楽です。

先ずは、リクエストからトークンを取得できるように Clova::Request を改造しましょう。 基本的には delegator を追加するだけです。 (必要な部分のみを記しています)

# app/models/clova/request.rb

module  Clova
  class Request
    extend ::Forwardable
    attr_accessor :context, :request, :session, :version

    def_delegators :@request, :event?, :intent?, :launch?, :session_ended?, :name, :slots, :payload
    def_delegators :@context, :access_token

    class Context
      extend ::Forwardable
      attr_accessor :audio_player, :system

      def_delegators :@system, :access_token

      class System
        extend ::Forwardable
        attr_accessor :application, :device, :user

        def_delegators :@user, :access_token
      end
    end
  end
end

次にコントローラを実装していきましょう。実装するのは次の3点です。 まずは context.System.user.accessToken にアクセストークンが来ているか確認しましょう。 2点目はトークンが有効かのチェックをしましょう。最後に、トークンにひもづくユーザデータを取得できるようにしましょう。

ユーザが存在しない場合や、トークンの期限切れになった時にトークン再発行を行うかなどはアプリ次第です。 ここでは「ユーザが存在しない場合はアカウント連携を行う旨のレスポンスを返す」「トークン期限切れ時はトークン再発行を行う」 というアプリの場合は、以下のような実装で十分でしょう。

# app/controller/clova_controller.rb

class ClovaController < ApplicationController
  def index
    clova_request = Clova::Request.parse_request_from(request.body.read)
    token = Doorkeeper::AccessToken.by_token(clova_request.access_token)

    return render json: say("アカウント連携してください") if token.nil?

    return render status: :forbidden, text: "access denied" unless token.accessible? # expired or revoked

    begin
      current_user = User.find(token.resource_owner_id)
    rescue ActiveRecord::RecordNotFound
      return render json: say("アカウント連携してください")
    end

    # 中略
  end
end

ここまでのコミットは こちら です。

Messaging API でメッセージを送ろう

さて、このブログも最後の機能説明になりました。最後は Clova スキルと LINE ボットの連携です。Messaging API を使うと、Rails アプリケーションから ユーザの LINE にメッセージを送ることができます。

Rails アプリケーションからのメッセージを送る場合には Messaging API の プッシュメッセージを送る 機能を用います。

送信の際にはいくつか制限事項があるので注意してください。 Clova スキルと連携する場合の制限事項はこちら に また、メッセージの送信リクエスト回数制限については こちら に それぞれ記載があります。

LINE Developers でチャネルを作成する

まずは Messaging API 用のチャネルを作成します。 使用するプロバイダーは Clova スキルの時と同一のものを使用してください。 プロバイダー選択後は Clova スキルの時と同じく、「新規チャネル作成」をクリックします

何のチャネルを作成するかの選択肢では、今回は Messaging API を選択します。選択するとチャネルの作成フォームが表示されますので、今までと同じように各項目を埋めていきます。

「プラン」の項目では「Developer Trial」を選択することに注意してください。

フォームの各項目を入力し終えたら「入力内容を確認する」ボタンをクリックしてください。 すると「情報利用に関する同意について」の確認ダイアログが表示されるので、内容を確認の上、「同意する」をクリックしてください。 入力内容の確認フォームが表示されます。

入力内容が正しければ、「LINE@利用規約の内容に同意します」「Messaging API(Developer Trial プラン)利用規約の内容に同意します」の両チェックボックスにチェックを入れ、 「作成」ボタンをクリックしてください。

すると、チャネルが作成され、Channel ID や Channel Secret などが発行されます。 Channel Secret は絶対に公開してはいけません

また、ページをスクロールしていくと「メッセージ送受信設定」より「アクセストークン(ロングターム)」が見つかるはずですので、確認してください。 最初は空白になっているかと思います。その場合は右側にある「作成」ボタンよりアクセストークンを作成してください。アクセストークンは絶対に公開してはいけません

さらにページをスクロールしていくと、このチャネルに紐づいた BOT アカウントを「友だち」として追加できる QR コードが見当たるはずです。 Clova と連携する場合の制限事項は などでも述べられていますが、 Push message を送るためには Bot アカウントとそのユーザが「友だち」である必要があります。 開発用に「友だち」になりたい場合には、この QR コードを LINE アプリなどで読み込むと、簡単に「友だち」になることができます。

また、ページ最下部の「その他」にはあなたの User ID が記載されています。後述しますが Push message を送るためには、Client Secret、アクセストークン、及び User ID が必要です。 手元でテストするためには是非この User ID をお使いください。

以上でチャネルの作成は完了しました。 ところで、作成された Channel ID や Channel Secret 、アクセストークンなどは 公開してはいけない データでした。

公開してはいけない情報を Rails に設定するには Encrypted credentials を使います。

まず、下記のコマンドで暗号化されたファイルを開きます。

$ cd path/to/your/app
$ ./bin/rails credentials:edit

中身は単純な YAML ファイルですので各項目を、例えば下記のように記載します。 (サンプルコード中の YOUR_CHANNEL_ID, YOUR_CHANNEL_SECRET は適宜読み替えてください)

# crendentials.yml

line_bot:
  channel_id: YOUR_CHANNEL_ID
  channel_secret: YOUR_CHANNEL_SECRET
  access_token: YOUR_LONGTERM_TOKEN

ファイルを編集し終え、エディタを閉じると自動的に暗号化されて保存されます。

設定した各項目は Rails.application.credentials からアクセス可能です。 ですので、例えば下記のコマンドを実行することで動きが確かめられるはずです。

$ ./bin/rails r 'p Rails.application.credentials.line_bot[:channel_id]'
YOUR_CHANNEL_ID

line-bot-sdk-ruby を Rails アプリに組み込む

line-bot-sdk-ruby を使えばとても簡単に Messaging API を ruby から使用可能です。 Gemfile に依存を追加し、bundle コマンドでインストールしましょう。

gem 'line-bot-api'
$ bundle

Line::Bot::Client のインスタンスを取得するメソッドを app/models/line_bot に用意して、他のクラスから参照しやすくしておきましょう。 コードを下記のように用意しましょう。

# app/model/line_bot.rb

require 'line/bot'

class Linebot
  def self.new
    Line::Bot::Client.new { |config|
      config.channel_secret = Rails.application.credentials.line_bot[:channel_secret]
      config.channel_token =  Rails.application.credentials.line_bot[:access_token]
    }
  end
end

準備ができたら、早速、プッシュメッセージを送ってみましょう。 Line::Bot::Client#push_message メソッドから、プッシュメッセージを送信可能です。第1引数の LINE の User ID を、第2引数にメッセージを指定できます。 LINE の User ID は、 Clova からのリクエストでは context.System.User.userId にありますので、例えば、Clova へのユーザ発話を起因としてプッシュメッセージを送りたいのであれば、下記のようにします。

# app/controllers/clova_controller.rb

class ClovaController < ApplicationController
  def index
    clova_request = Clova::Request.parse_request_from(request.body.read)

    case
    when clova_request.intent?
      case clova_request.name
      when "ReadListsIntent"
        message = "本日は晴天なり #{current_user.email} さんようこそ")
        client = LineBot.new
        client.push_message(request.user_id, type: :text, text: message)
        render json: say(message)
      else
        render json: empty
      end
    else
      render json: empty
    end
  end
end

(説明が漏れていましたが、User ID にアクセスしやすくするように Clova::Request を次のように修正を入れています)

module  Clova
  class Request
    extend ::Forwardable
    attr_accessor :context, :request, :session, :version

    def_delegators :@request, :event?, :intent?, :launch?, :session_ended?, :name, :slots, :payload
-   def_delegators :@context, :access_token
+   def_delegators :@context, :access_token, :user_id

    class Context
      extend ::Forwardable
      attr_accessor :audio_player, :system

-     def_delegators :@system, :access_token
+     def_delegators :@system, :access_token, :user_id

      class System
        extend ::Forwardable
        attr_accessor :application, :device, :user

-       def_delegators :@user, :access_token
+       def_delegators :@user, :access_token, :user_id
      end
    end
  end
end

上記までの変更の動作を試すためには、まずは、 Bot アカウントと「友だち」になる必要があります。 先ほど作成された QR コードを LINE アプリで読み込んで「友だち」になってください。

その上で、Clova のスキルを実行してみると、下記のようにメッセージが飛んでくるはずです。

ここまでのコミットは  です。

また、これ以上は詳しく説明いたしませんが、 LINE Developers より Webhook URI を適切に設定すれば、 line-bot-sdk を使って Clova のように、Bot に話しかけたらレスポンスを返す実装も簡単に組み込めます。

具体的なサンプルとしては下記のように

  1. request.body をパースする
  2. シグネチャを検証する
  3. Line::Bot::Client#reply_message で返信メッセージを送る

の3ステップで実現可能です。

# app/controllers/line_controller.rb

class LineController < ApplicationController
  protect_from_forgery except: :index
  before_action :validate_request, only: :index

  def index
    # request.body の内容をパースする
    events = client.parse_event_from(request.body.read)

    events.each do |event|
      case event
      when Line::Bot::Event::Message
        # 取得した reply token を元に、メッセージを返信する
        client.reply_message(event['replyToken'], type: :text, text: "今、#{event.message['text']}とおっしゃいましたか?")
      end
    end
  end

  private

  def validate_request
    return true if Rails.env.test?

    # リクエストが本当に LINE の Messaging API 経由かシグネチャを検証する
    unless client.validate_signature(request.body.read, request.headers['HTTP_X_LINE_SIGNATURE'])
      logger.warn("Signature missing")
      render text: "Access denied", status: 403
      return false
    end
  end

  def client
    @client !!= LineBot.new
    @client
  end
end

まとめ

少し駆け足かつ細かく説明していきましたが、基本的にはチャネルを作成して、 API ライブラリを組み込むか自作するだけで LINE の各種 API を 既存の Ruby on Rails アプリケーションに組み込み可能です。 また、Ruby on Rails 及び CEK を使い、Web アプリケーションと Voice User Interface とを組み合わせれば、 それぞれの得意・不得意とする部分を補い合ってより便利な UX をエンドユーザに提供できると思います。 ぜひ、あなたの Rails アプリに 声で操作できる機能 を組み込んでみてください。

明日は LINE Security室の関水さんと愛甲さんによる「仮想通貨交換所に必要なセキュリティ入門」です。 思えば 2018 年は様々な仮想通貨交換所での流出事故などが相次ぎました。 どのようにセキュリティを高めていけばいいのか、興味深いですね!

Related Post