Actions on Google向けに作ったDialogflowボットをLINEボットにする

以前DialogflowでLINEボットを作成しました。

　[Dialogflowと連携してLINE Botを作る](https://qiita.com/poruruba/items/09563e251adef1e1e8af)

これでもとりあえず良いのですが、見た目がちょっとチープな気がします。

一方で、LINE Beaconでもボットを作成しました。

　[LINE Beaconを自宅に住まわせる](https://qiita.com/poruruba/items/a28476ad53b2232c789d)

ですが、Dialogflowは、type=messageのイベントには対応していますが、LINE Beaconやフォロー・フォロー解除イベントなどの特殊なイベントには対応していません。

また、LINEボットより、Actions on GoogleのボットをDialogflowで作ることの方が多いので、できれば、Actions on Google向けのボットの実装がLINEにも転送できればベターです。

ということで、タイトルにもある通り、「Actions on Google向けに作ったDialogflowボットをLINEボットにする」をしたいと思います。
Actions on Googleを前提にDialogflowボットを作っておけば、LINEにも転用できることを期待しています。
Actoins on Googleで、いくつかリッチレスポンスが定義されていますが、LINEでも表示できるように、その中でよく使う以下のレスポンスを、LINEに対応させます。

* SimpleResponse
* BasicCard
* List
* Carousel
* Suggestion

結果として、以下の流れになります。

＜基本形＞
　(発話) ⇔ Actions on Google ⇔ Dialogflow ⇔ RESTfulサーバ①

＜LINEの場合＞
　(発話) ⇔ LINE(or LINE Beacon) ⇔ RESTfulサーバ② ⇔ Dialogflow ⇔ RESTfulサーバ①

自作するRESTfulサーバのうち、RESTfulサーバ①は、対抗がActions on Googleであるとみなして動作します。RESTfulサーバ①とDialogflowは、基本形とLINEの場合で共通です。
かなめは、RESTfulサーバ②の部分で、Actions on Google向けのレスポンスをLINE用に変換します。

（以下参考情報）

LINE Developers
 https://developers.line.biz/console/

LINE Flex Message
　https://developers.line.biz/ja/reference/messaging-api/#flex-message

line-bot-sdk-node.js
　https://line.github.io/line-bot-sdk-nodejs/api-reference/client.html#methods

Dialogflow
　https://dialogflow.com/

nodejs-dialogflow
　https://github.com/googleapis/nodejs-dialogflow

actions on google sdk node.js
　https://github.com/actions-on-google/actions-on-google-nodejs

Actions on Google Responses
　https://developers.google.com/actions/assistant/responses


# まずは基本形を作る

まずは純粋に、Dialogflowと連携して、Actions on Googleからの発話に応答するようにします。

DialogflowコンソールからAgentを作成し、以下の5つのIntentを追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/36d3244d-0dd0-e828-7440-4b4f1522d1f8.png)

* SimpleResponse
　Training phrases ： シンプル

* BasicCard
　Training phrases ： ベーシック

* List
　Training phrases ： リスト

* Carousel
　Training phrases ： カルーセル

* Suggestion
　Training phrases ： サジェスチョン

すべて、WebhookでRESTfulサーバ①に飛ばすようにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/d5b94df7-de84-c140-01aa-54886d0e9055.png)

FullfillmentのWebhookのURLにこれから立ち上げるRESTfulサーバ①のURLを指定して、ENABLED状態にしておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/a7bf5df4-cad1-738d-1c00-6ed82e474272.png)

あとで、作成したAgentのProject IDを後で使うので、覚えておきます。
Agent名の右側の歯車をクリックすると表示されるページに記載されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b60d9577-dcfa-5975-731e-8e60a77b45d4.png)
 
それから、RESTfulサーバ②からDialogflowを呼び出すためのサービスアカウントの作成が必要です。
Project IDのところにある Google Cloud をクリックして、Google Cloud Platformコンソールを開きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/67dbd507-06ff-7f45-1609-2da66a838a41.png)

そして、左メニューから、「APIとサービス」 → 「認証情報」を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/90807397-616f-9715-34b8-b3dc5eaf31fb.png)

ここで、「認証情報を作成」ボタンを押下し、サービスアカウントキーを選択します。
サービスアカウントとして、「新しいサービスアカウント」、サービスアカウント名に適当な名前、役割にはとりあえず「Project→オーナー」を選択しておきます（後でちゃんと役割を絞ってください）。キータイプはJSONを選びます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/fb66492d-2345-1049-cf27-35853866fcd8.png)

そうすると、シークレット情報が書かれたJSONファイルがダウンロードされます。
このファイルは後で使います。

# RESTfulサーバ①を立ち上げる

まずは、RESTfulサーバ①の実装を示します。
以下のnpmモジュールを使っています。

* actions-on-google

```js:index.js
'use strict';

const {dialogflow, SimpleResponse, BasicCard, Image, Button, List, Carousel, Suggestions} = require('actions-on-google');
const app = dialogflow({debug: true});

app.intent('SimpleResponse', (conv) =>{
    conv.ask('これはSimpleResponseに対する応答です。');
    conv.ask(new SimpleResponse({
        speech: '音声です。(LINE未対応)',
        text: 'テキストです。'
    }));
});

app.intent('BasicCard', (conv) =>{
    conv.ask('これはBasicCardに対する応答です。');

    conv.ask(new BasicCard({
        title: 'じゃんけん',
        subtitle: 'じゃんけんゲーム',
        text: 'じゃんけんゲームです。',
        image: new Image({
            url: "https://github.com/poruruba/media/blob/master/rock_paper_scissors.png?raw=true",
            alt: "アイコン画像"
        }),
        buttons: new Button({
            title: 'Wikipediaはこちら',
            url: 'https://ja.wikipedia.org/wiki/%E3%81%98%E3%82%83%E3%82%93%E3%81%91%E3%82%93'
        }),
        display: 'CROPPED'
    }));
});

app.intent('Carousel', (conv) =>{
    conv.ask('これはCarouselに対する応答です。');

    conv.ask(new Carousel({
        items: {
            ['グー'] : {
                synonyms:[
                    'グー'
                ],
                title: 'じゃんけんグー',
                description: 'グーを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/rock.png?raw=true',
                    alt:'じゃんけんグー'
                })
            },
            ['チョキ'] : {
                synonyms:[
                    'チョキ'
                ],
                title: 'じゃんけんチョキ',
                description: 'チョキを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/scissors.png?raw=true',
                    alt:'じゃんけんチョキ'
                })
            },
            ['パー'] : {
                synonyms:[
                    'パー'
                ],
                title: 'じゃんけんパー',
                description: 'パーを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/paper.png?raw=true',
                    alt:'じゃんけんパー'
                })
            }
        }
    }));
});

app.intent('List', (conv) =>{
    conv.ask('これはListに対する応答です。');

    conv.ask(new List({
        title: 'じゃんけんの手を選んでください。',
        items: {
            ['グー'] : {
                synonyms:[
                    'グー'
                ],
                title: 'じゃんけんグー',
                description: 'グーを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/rock.png?raw=true',
                    alt:'グー1'
                })
            },
            ['チョキ'] : {
                synonyms:[
                    'チョキ'
                ],
                title: 'じゃんけんチョキ',
                description: 'チョキを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/scissors.png?raw=true',
                    alt:'じゃんけんチョキ'
                })
            },
            ['パー'] : {
                synonyms:[
                    'パー'
                ],
                title: 'じゃんけんパー',
                description: 'パーを出します。',
                image: new Image({
                    url: 'https://github.com/poruruba/media/blob/master/paper.png?raw=true',
                    alt:'じゃんけんパー'
                })
            }
        }
    }));    
});

app.intent('Suggestion', (conv) =>{
    conv.ask('これはSuggestionに対する応答です。');

    conv.ask('じゃんけんの手を選んでください。');
    conv.ask(new Suggestions(['グー', 'チョキ', 'パー']));
});

exports.fulfillment = app;
```

以下のXXXXXXにインテント名を指定して、関数の中身で対応するレスポンスを作成しています。

> app.intent('XXXXXXX', (conv) =>{
> });

試しに、AndroidのActions on Googleから呼び出してみます。

> ＞ テスト用アプリにつないで
> ＜ こんにちは。
> ＞ベーシック

と呼び出すことで、BasicCardインテントが呼び出されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0dc14d6b-7b57-f91e-c311-9d73e7211c7e.png)

# LINEボットを設定する

それではこれから、LINEアプリとの連携に進みます。

LINE Developersコンソールから、プロバイダを作成します。（まだ作成していない場合）
　https://developers.line.biz/console/

そして、新規チャネルを作成します。
チャネルはMessaging APIを選択します。
プランは、フリーを選択しました。
アクセストークン(ロングターム)はまだ発行していないと思いますので、「再発行」ボタンを押下します。
Webhook送信は、「利用する」を選択しておきます。
Webhook URLは、これから立ち上げるRESTfulサーバ②のURLを指定します。
自動応答メッセージは、「利用しない」にしておきます。

Channel Secretとアクセストークン(ロングターム)は後で使うので、メモっておきます。

# LINE Flex Messageをデザインする

Actions on Googleで定義されているリッチレスポンスと同じようなUIをLINEでも表示させたいのですが、ぴったり一致するものはなかっため、LINE Flex Messageで表現します。

以下の、「Flex Message Simulator」を使うと、実際のUI画面を見ながら作れます。
　https://developers.line.biz/console/fx/

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/c57c12e3-89e2-1ec8-6ad5-cda1b8d9933b.png)

以下のリッチレスポンスのUIを作成します。Suggestionsは画面がないので、作成不要です。

* SimpleResponse
* BasicCard
* List
* Carousel

作成したこのJSONをRESTfulサーバ①からの応答内容をもとに動的に作成する必要があるため、このJSONを参考にして、のちほどのソースコードに埋め込んでいます。


# RESTfulサーバ②を立ち上げる

RESTfulサーバ②は、LINEアプリからのメッセージを受け付け、Dialogflowに転送します。逆に受け取ったレスポンスをLINE用に変換して戻します。
そこで、以下のnpmモジュールを使いました。

* @line/bot-sdk
* dialogflow
* uuid

2点補足します。

1点目は、Dialogflowに転送してからその応答が返ってきますが、その内容は、Actions on Google用に最適化されています。
具体的には、ProtoBuf形式になっています。それをJSON形式にパースしたのち、LINEのレスポンス形式に変換しています。
パースには、nodejs-dialogflowに含まれているstructjson.jsを流用させていただきました。

 https://github.com/googleapis/nodejs-dialogflow/blob/master/samples/structjson.js

（このJSファイルのおかげでだいぶ助かりましたし、見つけるのにも結構時間がかかりました。余裕があれば、Google protobufはどんなものか、responses[0].queryResult.webhookPayload の中身を見てみてください）

2点目は、LINEメッセージの受信をフィルタリングするため、LineUtilsというクラスを作成しています。

以下の関数の部分で、type=messageのメッセージを処理します。
処理は、Dialogflowへの呼び出しと、レスポンスのLINE用変換です。

> app.message((event, client) =>{
> });

LINE Beaconに対応する場合は、以下の関数実装で、type=beaconのメッセージイベントを処理します。

> app.beacon((event, client) =>{
> });

(app.XXXXXの参考情報)
[LINE Beaconを自宅に住まわせる](https://qiita.com/poruruba/items/a28476ad53b2232c789d)

```js:index.js
const config = {
    channelAccessToken: 【LINEのアクセストークン(ロングターム)】,
    channelSecret: 【LINEのChannel Secret】
};

const LineUtils = require('../../helpers/line-utils');
const app = new LineUtils(config);

const uuid = require('uuid/v4');
const StructJson = require('../../helpers/structjson');

const PROJECT_ID = 【DialogflowのProject ID】;

const dialogflow = require('dialogflow');

const sessionClient = new dialogflow.SessionsClient();
const sessionId = uuid();
const sessionPath = sessionClient.sessionPath(PROJECT_ID, sessionId);

app.message((event, client) =>{
    console.log('app.message called', event.source.userId);
    if( event.message.type == 'text'){
        const request = {
            session: sessionPath,
            queryInput: {
                text: {
                    text: event.message.text,
                    languageCode: 'ja-JP',
                }
            }
        };
    
        return sessionClient.detectIntent(request)
        .then(responses =>{
            console.log('responses=', responses);
            if( !responses[0].queryResult.webhookPayload ){
                return client.replyMessage(event.replyToken, { type: 'text', text: responses[0].queryResult.fulfillmentText });
            }

            var json = StructJson.structProtoToJson(responses[0].queryResult.webhookPayload);
            console.log(json);

            var message_list = app.convertMessages(json.google);
            return client.replyMessage(event.replyToken, message_list );
        })
        .catch(error =>{
            console.log(error);
        });
    }else{
        console.log('Not supported');
    }
});

exports.handler = app.lambda();
```

以下、ユーティリティです。
line-util.jsで、Actions on Google向けのレスポンスメッセージを、LINE用に変換しています。

```js:line-util.js
'use strict';

const line = require('@line/bot-sdk');
const Response = require('./response');

class LineUtils{
    constructor(config){
        this.client = new line.Client(config);
        this.map = new Map();
    }

    message(handler){
        this.map.set('message', handler);
    }

    follow(handler){
        this.map.set('follow', handler);
    }

    unfollow(handler){
        this.map.set('unfollow', handler);
    }

    join(handler){
        this.map.set('join', handler);
    }

    leave(handler){
        this.map.set('leave', handler);
    }

    memberJoined(handler){
        this.map.set('memberJoined', handler);
    }

    memberLeft(handler){
        this.map.set('memberLeft', handler);
    }

    postback(handler){
        this.map.set('postback', handler);
    }

    beacon(handler){
        this.map.set('beacon', handler);
    }

    accountLink(handler){
        this.map.set('accountLink', handler);
    }

    things(handler){
        this.map.set('things', handler);
    }

    lambda(){
        return async (event, context, callback) => {
            var body = JSON.parse(event.body);

            return Promise.all(body.events.map((event) =>{
                if( (event.type == 'message') &&
                     (event.replyToken === '00000000000000000000000000000000' || event.replyToken === 'ffffffffffffffffffffffffffffffff' ))
                    return;

                var handler = this.map.get(event.type);
                if( handler )
                    return handler(event, this.client);
                else
                    console.log(event.type + ' is not defined.');
            }))
            .then((result) =>{
//                console.log(result);
//                return new Response(result);
                return new Response({});
            })
            .catch((err) => {
                console.error(err);
                const response = new Response();
                response.set_error(err);
                return response;
            });
        }
    }

    convertMessages(google){
        var has_suggestion = false;
        if( google.richResponse && google.richResponse.suggestions )
            has_suggestion = true;

        var message_list = [];
        if( google.richResponse && google.richResponse.items ){
            for( var i = 0 ; i < google.richResponse.items.length ; i++ ){
                if( google.richResponse.items[i].simpleResponse )
                    message_list.push(this.convertSimpleResponse(google.richResponse.items[i].simpleResponse));
            }
        }
        if( has_suggestion ){
            if( message_list.length == 0){
                console.log('suggestion condition error');
            }else{
                var text = message_list.pop();
                message_list.push(this.convertSuggestions(text.text, google.richResponse.suggestions));
            }
        }

        if( google.richResponse && google.richResponse.items ){
            for( var i = 0 ; i < google.richResponse.items.length ; i++ ){
                if( google.richResponse.items[i].basicCard ){
                    message_list.push(this.convertBasicCard(google.richResponse.items[i].basicCard));
                }else if( google.richResponse.items[i].simpleResponse ){
                    // already processed
                }else{
                    console.log('Not supported message type');
                }
            }
        }

        if( google.systemIntent && google.systemIntent.data ){
            if( google.systemIntent.data.listSelect )
                message_list.push(this.convertList(google.systemIntent.data.listSelect));
            else if( google.systemIntent.data.carouselSelect )
                message_list.push(this.convertCarousel(google.systemIntent.data.carouselSelect));
            else
                console.log('Not supported message type');
        }

        return message_list;
    }

    convertSuggestions(text, suggestions){
        return this.createSuggestion(text, suggestions);
    }

    /* list = [] */
    createSuggestion(text, list){
        var quick = {
            type: "text",
            text: text,
            quickReply: {
                items: []
            }
        };

        for( var i = 0 ; i < list.length ; i++ ){
            if( typeof list[i].title == 'string' || list[i].title instanceof String ){
                var action = {
                    type: 'action',
                    action: {
                        type : 'message',
                        label: list[i].title,
                        text: list[i].title
                    }
                };
                quick.quickReply.items.push(action);
            }else{
                console.log('Not supported');
            }
        }

        return quick;
    }

    convertSimpleResponse(simpleResponse){
        var message = simpleResponse.displayText;
        if( !message ) 
            message = simpleResponse.textToSpeech;
        return this.createSimpleResponse(message);
    }

    convertBasicCard(basicCard){
        var button = basicCard.buttons[0];
        return this.createBasicCard(basicCard.title, basicCard.subtitle, basicCard.image.url, basicCard.formattedText, button.title, button.openUrlAction.url );
    }

    convertList(listSelect){
        var list = [];
        for( var i = 0 ; i < listSelect.items.length ; i++ ){
            list.push({
                title: listSelect.items[i].title,
                desc: listSelect.items[i].description,
                image_url: listSelect.items[i].image.url,
                message: listSelect.items[i].optionInfo.key
            });
        }
        return this.createList(listSelect.title, list );
    }

    convertCarousel(carouselSelect){
        var list = [];
        for( var i = 0 ; i < carouselSelect.items.length ; i++ ){
            list.push({
                title: carouselSelect.items[i].title,
                desc: carouselSelect.items[i].description,
                image_url: carouselSelect.items[i].image.url,
                message: carouselSelect.items[i].optionInfo.key
            });
        }
        return this.createCarousel(list);
    }

    createSimpleResponse(text){
        return { type: 'text', text: text };
    }

    createBasicCard(title, sub_title, image_url, text, btn_text, url ){
        var flex = {
            type: "flex",
            altText: title,
            contents: {
                type: "bubble",
                hero: {
                    type: "image",
                    url: image_url,
                    size: "full"
                },
                body: {
                    type: "box",
                    layout: "vertical",
                    contents: [
                        {
                            type: "box",
                            layout: "vertical",
                            contents: [
                                {
                                    type: "text",
                                    text: title,
                                    weight: "bold",
                                    size: "md"
                                },
                                {
                                    type: "text",
                                    text: sub_title,
                                    color: "#aaaaaa",
                                    size: "xs",
                                    wrap: true
                                },
                                {
                                    type: "spacer",
                                    size: "sm"
                                }
                            ]
                        },
                        {
                            type: "text",
                            text: text,
                            size: "sm",
                            wrap: true
                        }
                    ]
                },
                footer: {
                    type: "box",
                    layout: "vertical",
                    contents: [
                        {
                            type: "button",
                            style: "link",
                            height: "sm",
                            action: {
                                type: "uri",
                                label: btn_text,
                                uri: url
                            }
                        }
                    ],
                    flex: 0
                }
            }
        }

        return flex;
    }

    /* list = [ { title, desc, image_url, message } ] */
    createList(title, list){
        var flex = {
            type: "flex",
            altText: title,
            contents: {
                type: "bubble",
                styles: {
                    header: {
                        backgroundColor: "#eeeeee"
                    }
                },
                header: {
                    type: "box",
                    layout: "horizontal",
                    contents: [
                        {
                            type: "text",
                            text: title,
                            size: "sm",
                            color: "#777777"
                        }
                    ]
                },
                body: {
                    type: "box",
                    layout: "vertical",
                    contents: []
                }
            }
        };

        for( var i = 0 ; i < list.length; i++ ){
            if( i != 0 ){
                flex.contents.body.contents.push({
                    type: "separator",
                    margin: 'md'
                });
            }

            var option = {
                type: "box",
                layout: "horizontal",
                margin: 'md',
                contents: [
                    {
                        type: "box",
                        layout: "vertical",
                        flex: 4,
                        contents: [
                            {
                                type: "text",
                                weight: "bold",
                                text: list[i].title,
                                size: "sm"
                            },
                            {
                                type: "text",
                                text: list[i].desc,
                                color: "#888888",
                                size: "xs",
                                wrap: true
                            }
                        ]
                    },
                    {
                        type: "image",
                        url: list[i].image_url,
                        size: "sm",
                        flex: 1
                    }
                ],
                action: {
                    type: "message",
                    text: list[i].message
                }
            };

            flex.contents.body.contents.push(option);
        }

        return flex;
    }

    /* list = [title, desc, image_url, message] */
    createCarousel(list){
        var flex =  {
            type: "flex",
            altText: "#",
            contents: {
                "type": "carousel",
                "contents": []
            }
        };

        for( var i = 0 ; i < list.length ; i++ ){
            var option = {
                type: "bubble",
                hero: {
                    type: "image",
                    url: list[i].image_url,
                    size: "full"
                },
                body: {
                    type: "box",
                    layout: "vertical",
                    contents: [
                        {
                            type: "box",
                            layout: "vertical",
                            contents: [
                                {
                                    type: "text",
                                    text: list[i].title,
                                    weight: "bold",
                                    size: "sm"
                                },
                                {
                                    type: "text",
                                    text: list[i].desc,
                                    color: "#aaaaaa",
                                    size: "xs",
                                    wrap: true
                                }
                            ]
                        }
                    ],
                    action: {
                        type: "message",
                        text: list[i].message
                    }
                }
            };

            flex.contents.contents.push(option);
        }

        return flex;
    }
};

module.exports = LineUtils;
```

```js:response.js
class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = JSON.stringify(content);        
        return this;
    }
    
    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;
```

以下の部分は、環境に合わせて修正してください。
　【LINEのアクセストークン(ロングターム)】
　【LINEのChannel Secret】
　【DialogflowのProject ID】

ここで1つ忘れずにやっておくことがあります。
Google Cloud Platformコンソールで作成したシークレット情報ファイルを参照できる場所に置きます。
その場所を環境変数「GOOGLE_APPLICATION_CREDENTIALS」に示しておきます。
例えば、こんな感じです。

　GOOGLE_APPLICATION_CREDENTIALS="./cert/google/bottest-XXXXX-XXXXXXXXX.json"


# LINEアプリからアクセスしてみる

まずは、LINEアプリから、作成したチャネルとLINEボットとして友達登録します。
LINE Developersコンソールのチャネルの基本情報にあるQRコードを使うのが楽ちんです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/92f3d7c2-7667-0adc-dd02-9343a8978c2d.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/8839b403-0622-b4c1-9689-12e3fd646f74.png)

友達登録したのち、「シンプル」「ベーシック」「リスト」「カルーセル」「サジェスチョン」を試してみてください。Actions on GoogleとそっくりのUIが返ってくることがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b3740a82-deb9-78fe-9b49-57731e8dccbf.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/6af6238b-1dad-d522-9f02-c9d16f518cb6.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/cbf487bb-f7e7-5104-78f5-75c61e78b08d.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/40b9b847-4058-614d-1758-6bfeee01b33f.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/578ef446-b7fc-4bca-d8d6-117cbc64aea9.png)

以上です。