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 が設定済みで、また、User
, List
, Task
の 3 モデルが用意されています。 これらは User has many List
, List 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 に話しかけたらレスポンスを返す実装も簡単に組み込めます。
具体的なサンプルとしては下記のように
request.body
をパースする- シグネチャを検証する
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 年は様々な仮想通貨交換所での流出事故などが相次ぎました。 どのようにセキュリティを高めていけばいいのか、興味深いですね!