実装結果
実行結果は以下の通り、上手くデータを取ることができました。
テキストメッセージ以外は、固定メッセージを返すようにしています。
概要
先日、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へ飛ぶようにしてみようと思いました。
コード
'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がまだ良く分かっていない、自分の課題が見えました。
これからも書き続けてコツを掴めたら、もっとできることが増えていって楽しいだろうなぁと感じました。
(おすすめ提案とかもさせてみたかったけど、時間的に厳しかった。)