Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@PmanRabbit

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

実装結果

実行結果は以下の通り、上手くデータを取ることができました。
テキストメッセージ以外は、固定メッセージを返すようにしています。
tgtns-zwkp0.gif

概要

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

目的

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

環境

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

概要

自PCを(Node.js)サーバに見立て、LINEサーバからWebhookを受け取る為のトンネリングサービスはngrokを利用しています。
データの流れについては、公式ページ上記の参考サイトをみると何となく分かります。

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

コード

.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);

        // 関連の高い動画を3件返す
        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サーバーを実行中です…`);

苦戦した箇所

・YouTube APIの仕様理解
以下サイトを参考に、APIの有効化と仕様を確認しましたが、上手くデータを取れず少し苦戦しました。
原因は余計なパラメータを渡していた為でした。(APIの仕様書を読み解くのが、なかなか慣れない、、、)
 Youtube Data API Key の取得手順
 YouTube Data API | Google Developers
 動画の検索 | YouTube Data API v2

・LINE Messaging APIのカルーセル表示
テンプレートメッセージのカルーセル表示もかなり苦戦しました。YouTube APIからのデータは取れているのにエラーで上手くいかず、原因箇所特定に苦戦しました。
原因は、タイトルの文字数制限があり、超えていた為でした。なので、文字数判定を入れて無理やり切るように工夫してます。
    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がまだ良く分かっていない、自分の課題が見えました。
これからも書き続けてコツを掴めたら、もっとできることが増えていって楽しいだろうなぁと感じました。
(おすすめ提案とかもさせてみたかったけど、時間的に厳しかった。)

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
protoout-studio
プロトアウトスタジオは日本初のプロトタイピング専門スクールです。プログラミングだけではなく、企画力と発信力を身に付けて”自分で課題を見つけて実装し、発信し続ける人”を育成しています。 圧倒的なアウトプット力を身に付けましょう。 学生募集中です。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?