AizuHack LINEBot勉強会 Vol.3
資料一覧
- LINEBotとは
- AizuHack LINEBot勉強会 Vol.1
- AizuHack LINEBot勉強会 Vol.2
- AizuHack LINEBot勉強会 Vol.3(本記事)
- AizuHack LINEBot勉強会 Vol.4
はじめに
こんにちは、会津大学学部二年のしんぶんぶんです。
本記事はAizuHackのLINEBot勉強会 Vol.3の資料になります。
今回は、より高度なBotが作れるようになる技術を学びます。
今回やる技術を全て習得すれば、理論上はだいたいどんなBotでも作れるようになります。
具体的には以下の技術について学びます。
- axiosを使った外部APIとの通信
-
SheetDBを使った簡易データベース- node-json-dbを使った簡易データベースに変更しました
- LINE Front-end Framework(LIFF)の使い方
外部APIとの通信
第2回まではLINE Messaging API内で完結することだけをやってきましたが、外部のAPIと連携することで作れるものの幅が圧倒的に広がります。
今回は、ちょっとまえ話題になった気象庁のAPIを使用して天気予報を取得してみます。
実装イメージは以下の通りです。
今回使用するAPIのレスポンスはこちらから確認できます。
APIでよく使うメソッドとしてGET, POST, PUT, DELETEの4つがあります。
GETはデータを取得する時、POSTはデータを送信する時、PUTはデータを更新・追加するとき、DELETEはデータを削除するときに使用します。
今回はデータを取得するため、GETを使います。
HTTPメソッドに関する解説はこちらの記事などがわかりやすいかと思います。
- Botのソースコードのディレクトリに移動して、
npm i axios
を実行する - text.jsの一番上の行に
import axios from 'axios';
と追記 - textEvent関数のswitch文に以下のcaseを追加
// 天気予報というメッセージが送られてきた時
case '天気予報': {
// axiosを使ってAPIにGETリクエストを送り、レスポンスのdataを変数resに格納
const weatherApiRes = (await axios.get('https://www.jma.go.jp/bosai/forecast/data/forecast/070000.json')).data;
// 返信するメッセージを作成
message = {
type: 'text',
text: `【天気予報】
${weatherApiRes[0].timeSeries[0].timeDefines[0]}: ${weatherApiRes[0].timeSeries[0].areas[2].weathers[0]}
${weatherApiRes[0].timeSeries[0].timeDefines[1]}: ${weatherApiRes[0].timeSeries[0].areas[2].weathers[1]}
${weatherApiRes[0].timeSeries[0].timeDefines[2]}: ${weatherApiRes[0].timeSeries[0].areas[2].weathers[2]}
`,
};
break;
}
これで完成です!Botに天気予報
と送ったら天気予報が返ってくるはずです。
node-json-dbの使い方
Botの会話にコンテキスト(文脈)を持たせたり、データを保持するにはデータベースを使う必要があります。
今回はjsonファイルを簡易的なデータベースとして使える、node-json-dbの使い方を解説します。
CRUDを実装してみよう
まずはCRUD(Create, Read, Update, Delete)を実装してみましょう。
実装イメージは以下の通りです。
- Botのソースコードのディレクトリに移動して、
npm i node-json-db
を実行する -
/db
にmyDB.json
を作成 -
myDB.json
に{}
を書き込んで保存 - 以下のコードを
text.js
のimport axios from 'axios';
の後に追加
import { JsonDB } from 'node-json-db';
import { Config } from 'node-json-db/dist/lib/JsonDBConfig.js';
const myDB = new JsonDB(new Config('db/myDB.json', true, true, '/'));
5.以下のコードをtext.js
のtextEvent
関数内の先頭行に追加
// ユーザーIDを取得
const { userId } = event.source;
6.以下のコードをtext.js
のtextEvent
関数のswitch文に追加
// 'Create'というメッセージが送られてきた時
case 'Create': {
// DBにtestDataが存在しているかをチェック
try {
// DBからデータを取得(データがない場合は例外が投げられるのでcatchブロックに入る)
myDB.getData(`/${userId}/testData`);
// 返信するメッセージを作成
message = {
type: 'text',
text: 'データはすでに存在しています',
};
} catch (_) {
// 日時を取得
const date = new Date();
// DBにデータを追加
myDB.push(`/${userId}/testData`, `Data created at ${date}`, false);
// 返信するメッセージを作成
message = {
type: 'text',
text: 'データが作成されました',
};
}
break;
}
// 'Read'というメッセージが送られてきた時
case 'Read': {
// DBにtestDataが存在しているかをチェック
try {
// DBからデータを取得(データがない場合は例外が投げられるのでcatchブロックに入る)
const dbData = myDB.getData(`/${userId}/testData`);
// 返信するメッセージを作成
message = {
type: 'text',
text: `DBには以下のデータが保存されています\n\n${JSON.stringify(dbData)}`,
};
} catch (_) {
// 返信するメッセージを作成
message = {
type: 'text',
text: 'DBにデータが保存されていません',
};
}
break;
}
// 'Update'というメッセージが送られてきた時
case 'Update': {
// DBにtestDataが存在しているかをチェック
try {
// DBからデータを取得(データがない場合は例外が投げられるのでcatchブロックに入る)
myDB.getData(`/${userId}/testData`);
// 日時を取得
const date = new Date();
// DBのデータをアップデート(すでにデータがあるときにpushすると上書きされる)
myDB.push(`/${userId}/testData`, `Data created at ${date}`, false);
// 返信するメッセージを作成
message = {
type: 'text',
text: 'データを更新しました',
};
} catch (_) {
// 返信するメッセージを作成
message = {
type: 'text',
text: 'DBにデータが保存されていません',
};
}
break;
}
// 'Delete'というメッセージが送られてきた時
case 'Delete': {
// DBにtestDataが存在しているかをチェック
try {
// DBからデータを取得(データがない場合は例外が投げられるのでcatchブロックに入る)
myDB.getData(`/${userId}/testData`);
// DBからデータを削除
myDB.delete(`/${userId}/testData`);
// 返信するメッセージを作成
message = {
type: 'text',
text: 'DBのデータを削除しました',
};
} catch (_) {
// 返信するメッセージを作成
message = {
type: 'text',
text: 'DBにデータが保存されていません',
};
}
break;
}
上から順番にCreate, Read, Update, Deleteのコードになっています。
それぞれのコードを実行して動きを確認してみましょう。
/
が区切り文字になっており、例えば/U6b53f4ad79a23f5427119cb44f08dbd7/testData
にデータを保存した場合以下のような形になります。
{
"U6b53f4ad79a23f5427119cb44f08dbd7": {
"testData": "Data created at Tue Jun 14 2022 01:36:29 GMT+0000 (Coordinated Universal Time)"
}
}
コンテキスト管理を実装してみよう
現在みなさんが作っているBotは、1回メッセージを送ってそれに返信して終わりのため、連続的な会話を実装することができません。今回はコンテキスト管理を実装して会話状態を保持できるような仕組みを作っていきます。仕組みは以下の通りです。
-
メモ開始
と送ったらデータ保存モードになる - 送ったメッセージがメモとしてDBに保存されて、データ保存モードが終了する
-
メモ
と送ったらメモが表示される
実装イメージは以下の通りです。
-
text.js
のconst myDB = new JsonDB(new Config('db/myDB.json', true, true, '/'));
の下に以下のコードを追加する
const contextDB = new JsonDB(new Config('db/contextDB.json', true, true, '/'));
const messageDB = new JsonDB(new Config('db/messageDB.json', true, true, '/'));
2./db
以下にcontextDB.json
, messageDB.json
を作って{}
を書き込む
3.text.js
のtextEvent
関数内の、const { userId } = event.source;
の下に以下のコードを追記します
// DBからユーザーのデータを取得
let contextData;
let memoData;
try {
contextData = contextDB.getData(`/${userId}/context`);
} catch (_) {
contextData = undefined;
}
try {
memoData = messageDB.getData(`/${userId}/memo`);
} catch (_) {
memoData = undefined;
}
// contextDataで条件分岐
switch (contextData) {
// もしそのユーザーのcontextがmemoModeだったら
case 'memoMode': {
// すでに保存されているメモがDBにある場合
if (memoData) {
// すでにあるmemoカラムに新しいメッセージを追加する
memoData.push(event.message.text);
// メッセージをDBへ保存
messageDB.push(`/${userId}/memo`, memoData);
} else {
// memoカラムを作成してDBに保存
messageDB.push(`/${userId}/memo`, [event.message.text]);
}
// contextをDBから削除
contextDB.delete(`/${userId}/context`);
// 返信するメッセージをreturnする
return {
type: 'text',
text: `"${event.message.text}"というメッセージをdbに追加しました`,
};
}
default:
break;
}
4.text.js
のtextEvent
関数内のevent.message.text
のswitch文に以下のコードを追加します
// 'メモ'というメッセージが送られてきた時
case 'メモ': {
// メモのデータがDBに存在する時
if (memoData) {
// 返信するメッセージを作成
message = {
type: 'text',
text: `メモには以下のメッセージが保存されています\n\n${memoData}`,
};
} else {
// 返信するメッセージを作成
message = {
type: 'text',
text: 'メモが存在しません',
};
}
break;
}
// 'メモ開始'というメッセージが送られてきた時
case 'メモ開始': {
// DBにcontextを追加
contextDB.push(`/${userId}/context`, 'memoMode' );
// 返信するメッセージを作成
message = {
type: 'text',
text: 'メモモードを開始しました',
};
break;
}
これで完成です!実際にメッセージを送信してどんな挙動をするのか試してみましょう。
LINE Front-end Framework(LIFF)について
LIFFとは、LINEBotと連携したWebアプリケーションが簡単に作れるものです。
Webアプリ上でユーザーのプロフィールを取得したり、トークルームにメッセージを送ったりできます。
LIFFに関する詳細は公式ドキュメントから確認できます。
今回はWebアプリをこちらで用意しているので、それを使っていきましょう。
LINEログインのチャネルを作成しよう
- LINE Developersのコンソールにアクセス
- プロバイダーを選択
-
新規チャネル作成
を選択 -
LINEログイン
を選択 - チャネル名、チャネル説明は適当なものを入力し、
アプリタイプ
はWebアプリを選択 - 利用規約に同意して、チャネルを作成
LIFFを作成しよう
- 先ほど作成したチャネルのページに移動する
- LIFFタブを開いてLIFFアプリを追加する
- LIFFアプリ名は適当な物を入力し、サイズは
full
、エンドポイントURLはngrokのURLに/liff
を追加したもの、Scopeはprofile
とopenid
とchat_message.write
にチェックを入れ、ボットリンク機能はオフ、Scan QRはオンにして追加ボタンを押す - シェアターゲットピッカーをオンにする
- LIFF IDをコピーして、liffディレクトリのindex.htmlの
LIFFID
を自分のLIFF IDに書き換える - チャネルの画面からLIFF URLをコピーする
- LIFF URLをBotとのトーク画面に送って、そこからリンクを開く
- 自分のプロフィールなどが表示されれば成功!
使える機能の解説
- liff.sendMessages()
- Botとのトーク画面にメッセージを送信できます
- liff.shareTargetPicker()
- 任意のトーク画面(例えば他の友達やグループ)にメッセージを送信できます
- liff.getProfile()
- ユーザーのuserId、displayName(LINEのアカウント名)、pictureUrl(プロフィール画像のURL)、statusMessage(一言コメント)を取得できる
- liff.getOS()
- ユーザーが使っているOSが取得できる
- liff.getLanguage()
- ユーザーが使っている言語が取得できる
- liff.getVersion()
- LIFFのバージョンが取得できる
- liff.getLineVersion()
- ユーザーが使っているLINEのバージョンが取得できる
- liff.isInClient()
- LIFFをLINEのアプリ内から実行しているかどうか取得できる
- liff.isLoggedIn()
- ユーザーがログインしているかどうか取得できる
- liff.getContext()
- LIFFアプリが起動された画面(1対1のトーク、グループ、トークルーム、または外部ブラウザ)に関する情報が取得できる
- liff.getFriendship()
- ボットリンクをオンにすると、リンクしたbotとの友達関係が取得できる
- liff.scanCode()
- LINEのQRコードリーダを起動できる
- iOSでは使えない
- liff.closeWindow()
- liffを閉じる
演習課題
演習課題に取り組む際に役立ちそうなTips
-
JSONきれい
- JSONを貼り付けたら綺麗に整形してくれるツールです
- 初めて使うAPIはレスポンスの形がわからないので、いったんレスポンスを出力してJSONきれいで整形してみるとどんなレスポンスが返ってきているかを把握できます
- GETリクエストの場合はエンドポイントURLをブラウザのアドレスバーに入力するだけでJSONを取得することができます
-
Postman
- GUIぽちぽちでAPIを叩けるツールです
- ブラウザでも使えるし、インストールしても使えます
演習課題1
ToDoリストを作ってみましょう。要件は以下の通りです。
- タスクの追加, 一覧取得, タイトル更新, タスク削除ができる
- 一覧取得の時に、1から始めるインデックスをタスクの先頭に付与する
実装イメージは以下の通りです。
演習課題2
直近のニュース一覧を取得してみましょう。要件は以下の通りです。
-
NewsAPIを使用してください(アカウント登録が必要)
- 今回はTop headlinesを使用しましょう
- ドキュメントはこちらから確認できます
- ニュースを取得するときは5件まで取得してください
- たとえばTop headlinesを5件取得する場合は
https://newsapi.org/v2/top-headlines?country=jp&apiKey={API_KEY}&pageSize=5
にGETリクエストを投げてください - 画像URL(
urlToImage
)、タイトル、概要(description
)、公開日(publishedAt
)、掲載元(source.name
)、記事のURLをメッセージに含めてください
- 今回はTop headlinesを使用しましょう
-
App Keyは環境変数に入れてください
-
.env
というファイルにnewsApiKey=XXXXXXX
という形式で記述すると、jsコード上でprocess.env.newsApiKey
という形で呼び出せます - 環境変数が反映されない場合はサーバを再起動してみてください
-
-
レベル1: 受け取ったニュースをテキストメッセージで返してください。実装イメージは以下の通りです。
- レベル2: 受け取ったメッセージをFlexMessageで返してください
- 1ニュース1カラムのカルーセルにしてください
- FlexMessageのフィールドは空にできません(つまり、nullを指定することはできません)。そのため、APIから返ってきたレスポンスの各フィールドをチェックし、nullの場合はダミーの値をFlexMessageのフィールドに挿入してください。たとえば記事の画像URLは
urlToImage
というフィールドに入ってくるのですが、このフィールドがnullになっている場合、ダミーの値に差し替えずにそのままFlexMessageのフィールドに挿入すると、返信処理で400エラーになります。画像URLの場合はたとえばhttps://raw.githubusercontent.com/shinbunbun/aizuhack-bot/master/media/imagemap.png
というダミー値を使うことなどが考えられます。 - 記事のdescriptionが長いときにFlexMessageのレイアウトが崩れてしまうことがあります。100文字を超えた場合はそれ以降をカットするなどの処理を実装すると、綺麗に表示されられます。
実装イメージは以下の通りです
演習課題3(応用課題)
会津大学附属図書館の蔵書を検索するシステムを作ってみましょう。
要件は以下の通りです。
- 本のISBNを送信したら蔵書情報が返ってくるシステムを作りましょう
-
図書館APIを使用してください
- こちらから利用申請ができます
- アプリキーは環境変数に入れてください
- APIの仕様書はこちらです
- 会津大学図書館の蔵書であれば、
https://api.calil.jp/check?appKey={アプリキー}&isbn={書籍のISBN}&systemid=Univ_Aizu&callback=no
にGETリクエストを送ることで蔵書の取得ができます - 蔵書情報の取得に時間がかかる場合は、上記APIのレスポンスとしてセッションが返ってくるため、結果が取得できるまで
https://api.calil.jp/check?appKey={アプリキー}&session={セッション}
をループの中で2秒おきに叩いて、蔵書結果が返ってきたらループを抜けるという処理(ポーリング)を実装する必要があります
- 2秒待つ処理は以下の通りに実装できます
- 会津大学図書館の蔵書であれば、
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
- レベル1: 本の状態(貸出中かどうか)と、図書館の予約ページのリンク(レスポンスの
reserveurl
)、カリールのリンク(https://calil.jp/book/{ISBN}
)をテキストメッセージで返してみましょう- APIの規約上、カリールへのリンクを表示する必要があります。詳しくはAPIリファレンスをご覧ください
- 図書館の予約ページのリンクが
https://libeopsv.u-aizu.ac.jp/...
になっていますが、このままだと証明書エラーになりアクセスできないため、libeopsv
をlibopsv
に置換してから返信してください
レベル1の実装イメージは以下の通りです。
LINE_capture_677257065.379838.JPG
- レベル2: openBDのAPIを使って、本のタイトル、作者、出版社、カバー画像を取得し、レベル1で取得した情報と合わせてFlexMessageを作成してみましょう
-
https://api.openbd.jp/v1/get?isbn={ISBN}
にGETリクエストを送ることで本の情報が取得できます - レスポンスの
summary
カラムを参照すると良さげです
-
レベル2の実装イメージは以下の通りです。
- レベル3(発展): 現在はユーザーにISBNを送ってもらい、それを使って蔵書検索APIを叩く形になっています。書籍検索APIなどを使えば本のタイトルなどからISBNを取得できるため、それを使ってタイトル検索やフリーワード検索を実装してみましょう
- 本をフリーワード検索できるAPIは色々あるため、どれを使ってもOKです
- レベル4(発展): 現在の状態ではポーリングが発生した場合にユーザーへ返信するのが2秒以上遅れてしまうため、UXが悪いです。先に「検索中なので少々お待ちください」などのメッセージを返信し、後からPUSHメッセージで結果を返すように修正してみましょう
おわりに
皆さんお疲れ様でした。第4回では、GCPのCloud Runへのデプロイについて解説します。
第4回はこちら