LINE BOTでYouTubeの検索をしてみたよ

#実装結果
実行結果は以下の通り、上手くデータを取ることができました。
テキストメッセージ以外は、固定メッセージを返すようにしています。
![tgtns-zwkp0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/725039/745cc37b-5c74-5f13-fc36-3530f087636f.gif)

#概要
先日、LINE BOT の開発を教わったので、実際に作成してみました。
教わったのは簡単なテキストを返すものでしたが、LINE Messaging API でテンプレートメッセージのカルーセルを使ってみました。
LINE BOT の初期設定（事前準備）は以下を参考にしています。
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

#目的
私はよくYouTubeで動画を見るので、簡単なYouTube検索ボットを作ってみました。
単純メッセージのレスポンスだとつまらないと思ったので、カルーセル形式に挑戦してみました。

#環境
Visual Studio Code v1.49.0
node.js v14.9.0
ngrok v2.3.35

#概要
自PCを（Node.js）サーバに見立て、LINEサーバからWebhookを受け取る為のトンネリングサービスはngrokを利用しています。
データの流れについては、[公式ページ](https://developers.line.biz/ja/docs/messaging-api/overview/#messaging-api%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)や[上記の参考サイト](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)をみると何となく分かります。

今回、YouTubeのデータ検索にはYouTube API DATA v3 を利用し、LINE BOT に送ったメッセージでキーワード検索をしています。APIのレスポンスはaxiosで受け取り、関連する上位３件の動画情報から、サムネイル表示、タイトル表示、動画URLへ飛ぶようにしてみようと思いました。

#コード
```javascript:.js
'use strict'; // おまじない
// ########################################
//               初期設定など
// ########################################
// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');

const PORT = process.env.PORT || 3000;  // ローカル（自分のPC）でサーバーを公開するときのポート番号
const YoutubeURL = 'https://www.youtube.com/watch?v=';   // YouTubeURL
const YoutubeAPIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; //YouTubeAPIKey

// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
    channelSecret: '99999999999999999999999999999999',  //作成したBotのチャネルシークレット
    channelAccessToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' //作成したBotのチャネルアクセストークン
};

// ########## ▼▼▼ サンプル関数 ▼▼▼ ##########
const MainFunction = async (event) => {

    const userText = event.message.text;    // ユーザーメッセージ
    axios.defaults.baseURL = 'https://www.googleapis.com/youtube/v3';    // YouTube Data API

    // 「リプライ」を使って先に返事しておきます
    await client.replyMessage(event.replyToken, {
        type: 'text',
        text: '調べています……'
    });

    let Title = [];
    let IdUrl = [];
    let ImageUrl = [];
    try {
        // axiosでAPIを叩きます（少し時間がかかる・ブロッキングする）
        const res = await axios.get('/search?part=snippet&q=' + encodeURIComponent(userText) + '&key=' + YoutubeAPIKey);

        // 関連の高い動画を３件返す
        for (let i=0; i<3; i++) {
            Title.push(res.data.items[i].snippet.title);    // タイトル
            // 文字数が50より大きかったら末尾に...を付けたす　※MessagingApiの制限60文字を超えるとエラーになるので。
            if (Title[i].length > 50) {
                Title[i] = Title[i].substr(0, 50) + '...';
            }
            IdUrl.push(YoutubeURL + res.data.items[i].id.videoId);  // 動画URL
            ImageUrl.push(res.data.items[i].snippet.thumbnails.medium.url);    // サムネ画像
        }

    } catch (error) {
        // APIからエラーが返ってきたらターミナルに表示する
        return client.pushMessage(event.source.userId, {
            type: 'text',
            text: '検索中にエラーが発生しました。ごめんね。',
        });
        console.error(error);
    }

    // 「プッシュ」で後からユーザーに通知（カルーセル形式ガリ書き。）
    return client.pushMessage(event.source.userId, {
        "type": "template",
        "altText": "this is a carousel template",
        "template": {
            "type": "carousel",
            "columns": [
                {
                  "thumbnailImageUrl": ImageUrl[0],
                  "text": Title[0],
                  "defaultAction": {
                      "type": "uri",
                      "label": "動画を見に行く",
                      "uri": IdUrl[0]
                  },
                  "actions": [
                      {
                          "type": "uri",
                          "label": "動画を見に行く",
                          "uri": IdUrl[0]
                      }
                  ]
                },
                {
                  "thumbnailImageUrl": ImageUrl[1],
                  "text": Title[1],
                  "defaultAction": {
                      "type": "uri",
                      "label": "動画を見に行く",
                      "uri": IdUrl[1]
                  },
                  "actions": [
                      {
                          "type": "uri",
                          "label": "動画を見に行く",
                          "uri": IdUrl[1]
                      }
                  ]
                },
                {
                    "thumbnailImageUrl": ImageUrl[2],
                    "text": Title[2],
                    "defaultAction": {
                        "type": "uri",
                        "label": "動画を見に行く",
                        "uri": IdUrl[2]
                    },
                    "actions": [
                        {
                            "type": "uri",
                            "label": "動画を見に行く",
                            "uri": IdUrl[2]
                        }
                    ]
                  }
            ],
            "imageAspectRatio": "rectangle",
            "imageSize": "cover"
        }
      });
};
// ########## ▲▲▲ サンプル関数 ▲▲▲ ##########


// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################
// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であれば固定メッセージを返す
    if (event.type !== 'message' || event.message.type !== 'text') {
        //return Promise.resolve(null);
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: 'YouTubeで検索しようか？\n検索キーワードは？'
        });
    }
    // サンプル関数を実行します
    return MainFunction(event);
}


// ########################################
//          Expressによるサーバー部分
// ########################################
// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
    // Webhookの中身を確認用にターミナルに表示します
    console.log(req.body.events);

    // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
    if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') {
        res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
        console.log('検証イベントを受信しました！'); // ターミナルに表示します
        return; // これより下は実行されません
    }

    // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
    // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
    Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
```

#苦戦した箇所
<dl>
<dt>・YouTube APIの仕様理解</dt>
<dd>以下サイトを参考に、APIの有効化と仕様を確認しましたが、上手くデータを取れず少し苦戦しました。<br>
原因は余計なパラメータを渡していた為でした。（APIの仕様書を読み解くのが、なかなか慣れない、、、）<br>
　<a href="http://piyohiko.webcrow.jp/kids_tube/help/index.html">Youtube Data API Key の取得手順</a><br>
　<a href="https://developers.google.com/youtube/v3/docs/search?hl=ja">YouTube Data API | Google Developers</a><br>
　<a href="https://so-zou.jp/web-app/tech/web-api/google/youtube/data-api/">動画の検索 | YouTube Data API v2</a><br>
</dd><br>
<dt>・LINE Messaging APIのカルーセル表示</dt>
<dd>テンプレートメッセージのカルーセル表示もかなり苦戦しました。YouTube APIからのデータは取れているのにエラーで上手くいかず、原因箇所特定に苦戦しました。<br>
原因は、タイトルの文字数制限があり、超えていた為でした。なので、文字数判定を入れて無理やり切るように工夫してます。</dd>
</dl>
```javascript:
    Title.push(res.data.items[i].snippet.title);    // タイトル
       // 文字数が50より大きかったら末尾に...を付けたす　※MessagingApiの制限60文字を超えるとエラーになるので。
       if (Title[i].length > 50) {
            Title[i] = Title[i].substr(0, 50) + '...';
       }
```

#振り返り
課題はAPIの利用方法がまだ慣れない点だと強く感じました。
JavaScript、JSONがまだ良く分かっていない、自分の課題が見えました。
これからも書き続けてコツを掴めたら、もっとできることが増えていって楽しいだろうなぁと感じました。
（おすすめ提案とかもさせてみたかったけど、時間的に厳しかった。）