Dialogflowと連携してLINE Botを作る

みなさん、スマホにLINEアプリ入れてます？
（もし入れているのであれば、）せっかくLINEアプリをインストールしているので、いろんな用途に使えるように、LINE Botを作ってみたいと思います。
今回は、ちまたで流行っているトレンドキーワードを返すBotです。

ただ作るだけではモチベーションが上がらないので、以下の勉強もしたいと思います。

* 発話の入力処理として、ちょっとした自然言語解析も入れてみるために、「Dialogflow」を使います。
* トレンドキーワードを収集するために、「Twitter REST API」を使います。
* そして、LINE Botである「LINE Messaging API」を使います。

こんな感じで連携します。

　(発話) ⇔ LINEアプリ ⇔ (LINE Bot) ⇔ Dialogflow ⇔ RESTfulサーバ ⇔ Twitter REST API

以下のサイトにお世話になります。

Dialogflow Console
・https://console.dialogflow.com/

Twitter Developer Platform
・https://developer.twitter.com/

LINE Developers(プロバイダーリスト)
・https://developers.line.biz/console/

# LINEとDialogflowを連携させる

まずは、LINE Botを立ち上げて、応答をDialogflowで返してもらいましょう。
Dialogflow Consoleから、Agentを作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b1ccede9-42e0-96c1-3f86-8484f5d7d19e.png)

Agent名は、「TestBot」としました。
この状態ですでに、ある程度動きます。
右側にある「Try it now」と書いてあるところに、「こんにちは」と入力してEnterを押してみてください。
そうすると、こんにちは！ と返ってきます。これは、こんにちは、という言葉にDefault Welcome intentがマッチして、その応答が返ってきているわけです。

次にこの入力を、LINEから受け付けるようにします。
そのためには、左側の Integrationsを選択したのち、たくさんある候補の中から、LINEを選択します。そうすると、何やらLINEに関する情報を入力するフォームが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f207bf6d-125b-b982-b0ec-dff609e503a2.png)

それでは、LINE Developersのページを開いてみましょう。
まず最初に、チャネルを作成します。そのために、作成するプロバイダを選択しておきます。まだ、LINEのプロバイダーを作成していなければ作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/133875a5-4496-ff9e-84f5-3097b7a69bd7.png)

Botを作成するには、Messaging APIを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ca216f1f-418f-d8f8-17e2-0b488abef195.png)

アプリ名は適当に「テストボット」としました。
プランはどちらでもいいですが、フリーを選択しました。アプリの説明や大業種、小業種 を適当に入力してください。

作成が完了したら、これからいろいろ設定していきます。

チャネル基本設定を開きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/7ce17490-c747-3395-761f-32dc1b4a573e.png)

そうすると、所望の、「Channel ID」、「Channel Secret」、「アクセストークン(ロングターム)」があります。アクセストークン(ロングターム)はおそらくまだ生成されていないと思いますので、再発行ボタンを押下して生成します。（失効までの時間は0でよいです。）

これらを、DialogFlowに入力します。右上のスイッチをOnにしないと入力できません。
Webhook URLは後で使うので覚えておきます。最後にSTARTボタンを押下します。

一方、LINEのチャネル基本設定の方に戻って、設定を続けます。

Webhook送信を 利用する に編集します。
そして、Webhook URLに、Dialogflowで覚えておいたWebhook URLをコピペします。
これによって、LINEアプリで受け付けた発話をDialogflowに転送してくれるようになります。

これでひとまず準備ができました。
さっそく、ページの下の方にあるQRコード(アプリ検証時や友達に紹介する際にご活用ください)を、LINEアプリがインストールされているスマホでスキャンし、URLを開きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/5947dba4-043d-43ba-31c6-1b0b32309d12.png)

そうすると、LINEアプリが立ち上がって、友達の追加「テストボット」の画面が表示されます。追加ボタンを押下します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/8658a63d-1eeb-3d40-be05-cbf5a120ba08.png)

無事に追加されました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/bfc235ef-c2b4-c79d-4a10-60f95de35482.png)

テストボットに、「こんにちは」と入力すると、こんにちは！と返ってきました！
成功です！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/aeeb0c9a-7ee6-8db5-35bf-6720de3c76e7.png)

ただ、なんか「メッセージありがとうございます。申し訳ございませんが、このアカウントでは個別の返信ができないのです。次の配信をお楽しみに。」なんてのが一緒に送られてきました。しかも、発話するごとに。
これを消すには、LINEのチャネル基本設定の自動応答メッセージを「利用しない」に変更します。
また、初めて友達追加したときのメッセージも出ていましたが、これを変更するには、友達追加時あいさつ のところで文章を変更できます。

# TwitterにDeveloperアカウント登録する。

さあ、今度は、Twitter REST APIを使うための準備をします。
まずは、TwitterにDevelperアカウント登録します。(Tweetするユーザのためのアカウントではありません)

以下の方の投稿を参考にさせていただきました。ありがとうございました。

[新しくなった Twitter Developer ポータルに登録してみる](https://qiita.com/tdkn/items/521686c240b0c5bc6207)

英語で入力しないといけないので、ちょっと難儀ですが、今後何かと使うはず！！！なので、めげずに頑張って入力してください！


# Twitterにアプリを登録する。

今回、Twitter REST APIを使って、トレンドキーワードを取得します。具体的には、以下のAPIを利用します。

Get trends near a location
https://developer.twitter.com/en/docs/trends/trends-for-location/api-reference/get-trends-place

この呼び出しには、Consumer API keysである「API key」と「API secret key」が必要です。
これを入手するには、Twitter Developerからアプリの登録をする必要があります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/933fdd1d-5980-2de0-4ebb-714cb4d4e5fe.png)

入力するのは、(required)となっているところだけです。
App name、Application description、Website URL、Tell us how this app will be usedです。
Sign in with TwitterはDisableで、Callback URLやTerms of service URL、Privacy policy URLなどなどは使わないので、入力不要です。

無事に登録が完了したら、アプリの「Keys and tokens」を選択してください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b6fec8c8-a7fe-58e3-21f2-4f64a107bfeb.png)

ここに、Consumer API keysがありました。

# 単独でトレンドキーワードを取得する。

まずは特にDialogflowやLINEと連携せずに、単独でTwitter REST APIを呼び出してトレンドキーワードを取得してみましょう。

適当なフォルダを作成し、以下の二つのファイル(package.json、index.js)を作成します。

```index.js
var fetch = require('node-fetch');
const { URLSearchParams } = require('url');

const TWITTER_API_KEY = 【TwitterアプリのAPI key】;
const TWITTER_API_SECRET_KEY = 【TwitterアプリのAPI secret key】;
const YAHOO_WOEID = 【トレンドを取得したい場所のWOEID】;

function get_trendlist(){
    return do_post_urlencoded('https://api.twitter.com/oauth2/token', { grant_type: 'client_credentials'}, TWITTER_API_KEY, TWITTER_API_SECRET_KEY)
    .then(result =>{
//        console.log(result);
        var access_token = result.access_token;
        return do_get_token('https://api.twitter.com/1.1/trends/place.json', { id: YAHOO_WOEID }, access_token);
    })
    .then(result =>{
        var list = JSON.parse(result);
//        console.log(list);

        return list[0];
    });
}

function do_get_token(url, qs, token){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

//    console.log(url + '?' + params.toString());
    return fetch(url + '?' + params.toString(), {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        return response.text();
    });
}

function do_post_urlencoded(url, body, client_id, client_secret){
    var data = new URLSearchParams();
    for( var name in body )
        data.append(name, body[name]);

    var basic = new Buffer(client_id + ':' + client_secret).toString('base64');
    
    return fetch(url, {
        method : 'POST',
        body : data,
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Basic ' + basic }
    })
    .then((response) => {
        return response.json();
    });
}

get_trendlist()
.then(list =>{
	console.log(list);
});
```

```package.json
{
  "name": "twitter_test
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "node-fetch": "^2.3.0"
  }
}
```

index.jsを少し書き換えます。

> const TWITTER_API_KEY = 【TwitterアプリのAPI key】;
> const TWITTER_API_SECRET_KEY = 【TwitterアプリのAPI secret key】;
> const YAHOO_WOEID = 【トレンドを取得したい場所のWOEID】;

の部分です。
上2つはさきほど取得しました。
WOEIDは、どこの町のトレンドを取得するかを指定します。（確かに、ニューヨークでのトレンドを教えてもらっても日本に住んでる人にはピンときませんからね。。。）
以下のサイトが有用です。

WOEIDチェッカー(日本の都道府県のWOEID)
https://lab.syncer.jp/Tool/WOEID-Lookup/

たとえば、横浜と入力すると、複数選択肢が出てきました。今回は「横浜市 (Town) - 1118550」を選択しました。

モジュールをインストールするために、以下を実行します。

> npm install

いざ、実行します。

> node index.js

無事に、コンソールに、以下のような感じで取得できました。

```
{ trends:
   [ { name: '設営完了',
       url: 'http://twitter.com/search?q=%E8%A8%AD%E5%96%B6%E5%AE%8C%E4%BA%86',
       promoted_content: null,
       query: '%E8%A8%AD%E5%96%B6%E5%AE%8C%E4%BA%86',
       tweet_volume: 10947 },
     { name: '大阪万博',
       url: 'http://twitter.com/search?q=%E5%A4%A7%E9%98%AA%E4%B8%87%E5%8D%9A',
       promoted_content: null,
       query: '%E5%A4%A7%E9%98%AA%E4%B8%87%E5%8D%9A',
       tweet_volume: 262816 },
     { name: '#vimconf',
       url: 'http://twitter.com/search?q=%23vimconf',
       promoted_content: null,
       query: '%23vimconf',
       tweet_volume: null },
・・・・
```

# DialogflowとTwitterを連携させる

最後に、DialogflowとTwitterを連携させます。
今回は、Dialogflowの出力先がLINEであり、LINEアプリでかっこよく表示させるために、ちょっと変わった連携をします。具体的には、FollowupEventInputを使います。

早速、作業に取り掛かります。
Twitter REST APIの呼び出しは、Dialogflowの中ではできないので、DialogflowのFulfillmentでWebhookを使って別に立てるRESTfullサーバに転送します。
WebhookをEnableにして、URLを転送先のRESTfulサーバのURLを入力します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/cf923cc7-0b4c-ff60-ef76-43014d18812f.png)

次に、トレンドキーワードを要求するときのIntentを作成します。
たとえば、Intent名を「TrendwordIntent」とします。
Training phrasesに、ひっかけたい言葉をいくつか入力しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/5a0cc866-03fe-c96b-a0f0-8a40d55549a7.png)

そして、Fulfillmentの「Enable webhook call for this intent」をOnにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/5df08bc5-0f42-7db6-2b7a-22092ed75189.png)

これによって、Training phrasesで入力した言葉をLINEアプリで発話するとそれをひっかけて、Webhookで指定したURLに転送されます。

次は、RESTfullサーバの実装です。

Swagger定義は以下の通りです。というより、入力パラメータは何もありません。

```swagger.yaml
  /testbot:
    post:
      x-swagger-router-controller: routing
      operationId: testbot
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object
```

実装は以下の通りです。

```index.js
'use strict';

const {dialogflow} = require('actions-on-google');
const app = dialogflow({debug: true});

var fetch = require('node-fetch');
const { URLSearchParams } = require('url');

const TWITTER_API_KEY = 【TwitterアプリのAPI key】;
const TWITTER_API_SECRET_KEY = 【TwitterアプリのAPI secret key】;
const YAHOO_WOEID = 【トレンドを取得したい場所のWOEID】;
const NUM_OF_PICKUP = 5;

var trend_list = null;

app.intent('TrendwordIntent', async (conv) => {
    trend_list = await update_trendlist(trend_list);
    var pickup = pickup_list(trend_list.trends);

    var params = {};
    for( var i = 0 ; i < NUM_OF_PICKUP ; i++ )
        params['word' + (i + 1)] = pickup[i];

    conv.followup('WEBHOOK_RECEIVED', params );
});

function pickup_list(list){
    var pickup = [];
    for( var i = 0 ; i < NUM_OF_PICKUP ; i++ ){
        var name = list[i].name;
        if( name.slice(0, 1) == '#' || name.slice(0, 1) == '?' )
            name = name.slice(1);
        
        pickup.push(name);
    }

    return pickup;
}

async function update_trendlist(pickup){
    var now = new Date().getTime();
    if( !pickup || pickup.trends.length < NUM_OF_PICKUP * 2 || pickup.update_date < now - 1000 * 60 * 10 ){
        if( !trend_list || trend_list.update_date < now - 1000 * 60 * 10){
            trend_list = await get_trendlist();
            trend_list.trends = array_shuffle(trend_list.trends);
            trend_list.update_date = now;
        }
        return JSON.parse(JSON.stringify(trend_list));
    }else{
        pickup.trends = pickup.trends.slice(NUM_OF_PICKUP);
        return pickup;
    }
}

function array_shuffle(array){
    for(var i = array.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    return array;
}

function get_trendlist(){
    return do_post_urlencoded('https://api.twitter.com/oauth2/token', { grant_type: 'client_credentials'}, TWITTER_API_KEY, TWITTER_API_SECRET_KEY)
    .then(result =>{
        console.log(result);
        var access_token = result.access_token;
        return do_get_token('https://api.twitter.com/1.1/trends/place.json', { id: YAHOO_WOEID }, access_token);
    })
    .then(result =>{
        var list = JSON.parse(result);
        console.log(list);

        return list[0];
    });
}

function do_get_token(url, qs, token){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    console.log(url + '?' + params.toString());
    return fetch(url + '?' + params.toString(), {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        return response.text();
    });
}

function do_post_urlencoded(url, body, client_id, client_secret){
    var data = new URLSearchParams();
    for( var name in body )
        data.append(name, body[name]);

    var basic = new Buffer(client_id + ':' + client_secret).toString('base64');
    
    return fetch(url, {
        method : 'POST',
        body : data,
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Basic ' + basic }
    })
    .then((response) => {
        return response.json();
    });
}

exports.fulfillment = app;
```

npmモジュールとして、node-fetchとactions-on-googleを使っています。
以下、解説します。

function array_shuffle(array)
・取得したトレンドキーワード(50個ほど)をシャッフルしています。

async function update_trendlist(pickup)
・ここでトレンドキーワードを取得しているのですが、取得のたびにTwitter REST APIを呼び出しているのではなく、10分経過していなければ、以前に取得したトレンドキーワードを使います。ただし、LINEアプリには5個ずつトレンドキーワードを渡しているので、50個使い果たしたら時間に関係なく再度Twitter REST APIを呼び出して再取得しています。

function pickup_list(list)
・Twitter REST APIから取得したトレンドから、キーワード文字列のみを抜き出しています。先頭に#が入ってる場合があるのでそれも除きます。

app.intent('TrendwordIntent', async (conv))
・これがメインの関数です。
Dialogflowからインテント名「TrendwordIntent」という名前でインテントが飛んできますので、それをここでキャッチしています。

ここですごく重要なのが、conv.followup()です。
npmモジュールのactions-on-googleを使ったことがある方はわかると思いますが、通常はconv.askを使っていたかと思います。conv.followupを使うと、代わりに、DialogflowのFollowupEventInputが返るようになります。
第一引数がイベント名、後者がパラメータです。

(参考情報)
https://dialogflow.com/docs/events/custom-events

イベント名は、例えば「WEBHOOK_RECEIVED」としました。
パラメータの方は、ちょっと工夫をしています。
トレンドキーワード5個をそのままテキストで返すのではなく、以下のような形で返してます。

```
{
	"word1": 1番目のトレンドキーワード,
	"word2": 2番目のトレンドキーワード,
	"word3": 3番目のトレンドキーワード,
	"word4": 4番目のトレンドキーワード,
	"word5": 5番目のトレンドキーワード
}
```

この理由はこの後説明します。

最後に、以下がありますが、これは、npmモジュールであるactions-on-googleを使ったソースコードを、AWS API Gateway＋Lambdaの環境で動かすためのおまじないです。

> exports.fulfillment = app;

Lambdaに配置する場合は、Lambdaの関数のハンドラの名前を、index.handlerからindex.fulfillmentに変える必要があります。

参考情報
https://github.com/actions-on-google/actions-on-google-nodejs

それではRESTfulサーバを立ち上げて、Dialogflow Consoleに戻ります。

Dialogflowで、RESTfulサーバからの応答を受け取る処理を設定しましょう。
で、実はなんと先ほどTrendwordIntentでは受け取らないのです。RESTfulサーバからfollowupEventInputを返すと、Dialogflowの入力処理が受け付けて、別のインテントに振り分けられるのです。
どのインテントに振り分けられるかというと、Eventsに「WEBHOOK_RECEIVED」が指定されたインテントが選択されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/7519e846-cde3-db94-742c-123f52b5f45a.png)

このインテントで、conv.followup()で指定したパラメータを受け取ることができます。TrendwordResponseというインテント名を付けました。
Actions and parametersのところで、パラメータで指定された項目名をResponseのところで使う変数名(word?)に割り当てます。
ENTITYは、@sys.anyとします。
VALUEのところの書き方が特殊ですが、以下の感じです。

> ＃イベント名.項目名

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/4fb9c314-22b7-9f04-1ea9-0d7b5aaee379.png)

最後に、LINEに合わせて整形します。
Responsesのところを見ると、DEFAULTの隣に、LINEというタブが増えているのがわかります。これを選択することで、LINEに合わせた表示に変えてることができます。
DEFAULTにあるText Responseは不要なので削除します。
LINEタブを選択したのち、ADD RESPONSESボタンを押下すると、出力フォーマットとしていくつか選べそうです。今回は、Cardを選択しました。5個のトレンドキーワードなので5個作ります。

・Enter card title (required)のところには、$word? を入力します。(?は1から5の数字)

・Enter new button title のところには、Google検索 としておきましょう。

・そこで下矢印を選択すると Enter URL or text postback の入力欄がでてくるので、ここには https://www.google.com/search?hl=ja&q=$word? と入力しましょう。(?は1から5の数字)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b1be5076-fd5e-8cfb-c121-9b0070f9c0a1.png)

さあこれで完成です。
いよいよ、LINEアプリからトレンドキーワードを取得してみましょう。
先ほどと同じように、友達として登録済みのテストボットを選択し、トレンドキーワードと入力してみます！

トレンドキーワードと、Google検索と書かれたカードが5つ表示されましたでしょうか？
ここで、Google検索を選択すると、そのトレンドキーワードをGoogleページに飛んで検索してくれます。
 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/73303e79-1ccd-5607-955e-8cc70fd8b84b.png)

[別の表示方法]

別の表示方法もあります。（こっちのほうがLINE的に見やすいかも）
ただし、1カード辺り3個しかボタンを付けられないのでトレンドキーワードも1度に3個までです。

・Enter card title (required)のところには、最近のトレンドキーワードです。

・Enter new button title のところには、$word? を入力します。(?は1から3の数字)

・そして下矢印を選択して Enter URL or text postback の入力欄がでてくるので、ここには https://www.google.com/search?hl=ja&q=$word? と入力します。(?Enter new button titleと合わせます。)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/631f824e-24e0-26cd-151e-85606ec2528d.png)

LINEでの見え方です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/11507362-ba58-b106-2c08-1ae1c1860bbe.png)

ご参考までに、このAlexaスキル版もあります。
[トレンドキーワード](https://www.amazon.co.jp/dp/B07KQ5GKSH)

# おまけ

conv.followup() を使ったやりとりが面倒！ という方は、以下も参考にしてください。
Actions on Google に慣れている方であれば、こちらの方が楽だと思います。

　[Actions on Google向けに作ったDialogflowボットをLINEボットにする](https://qiita.com/poruruba/items/4f20e8f5cf2b0a4e9ee6)

あと、こちらもご参考まで。

　[LINEボットを立ち上げるまで。LINEビーコンも。](https://qiita.com/poruruba/items/c71c96ca83c7403c2930)

以上です。