野望
DBにNotionを用いたLINE Bot的なサムシングを作りたいと思い続けています。
前回までのあらすじ
LINEオウム返しBotを作ったり、Notion APIの検証をしたり。
今回やったこと
LINEのリッチメニューを使って追加か検索を選択し、以下の操作ができるようになりました。
・NotionにKeyとValueの組を追加する
・Notionに登録されたKeyとValueを検索する
また、今回はLINEでラリーを行いながら操作をするのでステータス管理も少しだけ行っています。
・追加時:「追加」選択→追加するデータを送信→追加完了
・検索時:「検索」選択→検索するキーワードを送信→検索結果返却
削除はまだできません。
過去の記事でLINEアカウント作成方法やAWSにおけるLambdaとAPI Gatewayの設定は書いていますので今回は割愛します。
手順
①Notionでテーブルの準備(ステータス管理テーブル編)
②Notionでテーブルの準備(データ本体用テーブル編)
③リッチメニューの準備
④Lambdaの処理ロジック検討
⑤実装
①Notionでテーブルの準備(ステータス管理テーブル編)
今回は以下のようにステータス管理のテーブルを作成して、最後に行った処理に応じて次の操作を制御しやすくしています。
・項目1:ステータス(ready/add/search)
・項目2:時刻
今回は一人で使う想定なのでこれだけの項目ですが、もっと複雑にしてもやりようはありそうです。
ex)複数人で使う場合はLINEのユーザID用の項目を用意する。
より複雑なラリーを行う場合はjsonなどのデータを保管する項目を用意する。
②Notionでテーブルの準備(データ本体用テーブル編)
過去回のテーブルをそのまま流用しています。
・項目1:key
・項目2:value
(・項目3:時刻 ←検索時に直近n個の値をとってくる使い方ができると思って用意した。今回は未使用。)
③リッチメニューの準備
LINE Official Manager > トークルーム管理 > リッチメニューにて簡単なメニューを用意します。
今回は画像を用意せずに提供されているテンプレートを使用しています。
アクションはテキストを選択しています。
(メニューを選択したことによって追加・検索の処理がトリガーされるというよりも、メニュー選択によって投稿されたテキストによって各処理が始まります。)
④Lambdaの処理ロジック検討
追加・検索処理の一連の流れは次のようになります。
・追加:追加メニューで追加フロー開始→キーと値の要求→キー・値投稿→登録完了
・検索:検索メニューで検索フロー開始→検索文字列の要求→検索文字列投稿→検索結果返却
上記の流れに加えて、①で記載した通り、想定外の操作がされた時に備えて以下のステータス管理をしています。
・add:追加フロー開始時
・search:検索フロー開始時
・ready:追加or検索フロー完了時、フロー中に想定外の操作が起こり処理を中断した時
⑤実装
NotionのAPI検証を行った際の記事は↓です。
https://qiita.com/DJROU/items/b7e159cbbecf93678df5
相変わらずPromiseやらasync/awaitが苦手です。
'use strict';
// セットアップ
const line = require('@line/bot-sdk');
const { Client } = require("@notionhq/client");
const notionToken = "Notionのトークン";
const databaseId = 'データ登録用テーブルのID';
const statusTableId = 'ステータス管理用テーブルのID';
const notion = new Client({
auth: notionToken,
});
const client = new line.Client({
channelAccessToken: 'LINEのアクセストークン'
});
exports.handler = async (event, context, callback) => {
// 受信タイプを取得
const type = JSON.parse(event.body).events[0].type;
const replyToken =JSON.parse(event.body).events[0].replyToken;
console.log(JSON.parse(event.body).events[0]);
// text以外の場合はやり取りを中断
if(type=="message" && JSON.parse(event.body).events[0].message.type != "text"){
console.log('データ受信タイプエラー');
const replyMessage = "正しい形式ではありません。最初からやり直してね。";
lineReplyText(replyToken, replyMessage);
addStatus("ready");
callback(null,"OK");
// typeがmessageかつテキストのときは追加/検索コマンドを確認する→入力内容とステータスに応じて処理
}else if(type=="message"){
console.log('textメッセージ');
let replyMessage = "";
let requestMessage = JSON.parse(event.body).events[0].message.text;
console.log(requestMessage);
const requestMessages = requestMessage.split(/\n/);
//追加or検索のときはステータスをadd or searchモードに変更
if(requestMessages[0] == "追加"){
console.log('追加処理開始');
replyMessage = "追加したい曲名、アーティスト名、コメントを3行に改行して送信してください。";
addStatus("add");
lineReplyText(replyToken, replyMessage);
callback(null, "処理成功");
return;
} else if(requestMessages[0] == "検索"){
console.log('検索処理開始');
replyMessage = "検索したい文字列を1行で入力してね。";
addStatus("search");
lineReplyText(replyToken, replyMessage);
callback(null, "処理成功");
return;
}
//notionのroustatustableで最後のレコードをとってきて分岐
let currentStatus = await statusCheck();
if(currentStatus == "add") {
console.log('addステータス');
if(requestMessages.length != 2 && requestMessages.length != 3) {
console.log('追加形式エラー');
replyMessage = "正しい形式で投稿してください。";
} else {
console.log('Notion追加開始');
replyMessage = await addItem(requestMessages[0], requestMessages[1]);
addStatus("ready");
}
lineReplyText(replyToken, replyMessage);
} else if(currentStatus == "search") {
//検索モードは1行で受付
if(requestMessages.length != 1) {
replyMessage = "1行で投稿してください。";
} else {
replyMessage = await searchPage(requestMessages[0]);
}
lineReplyText(replyToken, replyMessage);
} else {
replyMessage = "【追加/検索】の選択からやり直してね。";
addStatus("ready");
lineReplyText(replyToken, replyMessage);
}
callback(null, "処理成功");
}
};
function lineReplyText(replyToken, replyMessage) {
let message = {
type: 'text',
text: replyMessage
};
client.replyMessage(replyToken, message).then((data) => {
console.log(data);
}).catch((err) => {
console.log(err, "だめでし");
});
}
async function addItem(keyItem, valueItem) {
let returnMessage = "";
try {
const response = await notion.pages.create({
parent: { database_id: databaseId },
properties: {
key: {
title: [
{
text: {
content: keyItem,
},
},
],
},
value: {
rich_text: [
{
text: {
content: valueItem,
},
},
],
},
timestamp: {
number: Date.now()
}
}
});
console.log(response);
returnMessage = `key:${keyItem}・value:${valueItem}をメモに追加しました。`;
} catch (error) {
console.error(error.body);
returnMessage = `追加処理に失敗しました。詳細:${error.body}`;
}
return returnMessage;
}
async function searchPage(searchItem){
const response = await notion.search({
query: searchItem,
});
let searchResults = "以下の内容がヒットしました。";
for (let i =0; i < response.results.length; i++) {
let count = i + 1;
let searchResult = "\n【" + count + "件目】"
// + "\nid: "
// + response.results[i].id
+ "\nkey: "
+ response.results[i].properties.key.title[0].text.content
+ "\nvalue: "
+ response.results[i].properties.value.rich_text[0].text.content;
searchResults += searchResult;
}
addStatus("ready");
return searchResults;
}
// 進行中ステータス[add/search/delete]と完了/中断ステータス[ready]で管理
async function addStatus(statusName) {
try {
const response = await notion.pages.create({
parent: { database_id: statusTableId },
properties: {
status: {
title: [
{
text: {
content: statusName,
},
},
],
},
timestamp: {
number: Date.now()
}
}
});
console.log(response);
} catch (error) {
console.error(error.body);
}
return `ステータスを${statusName}に更新`;
}
//ステータス用テーブルの最新のデータ(=現在のステータス)をとってくる
async function statusCheck() {
const myPage = await notion.databases.query({
database_id: statusTableId,
sorts: [
{
"property": "timestamp",
"direction": "descending"
}
],
page_size:1,
});
console.log(myPage.results[0].properties.status.title[0].plain_text);
return myPage.results[0].properties.status.title[0].plain_text;
}
できあがり
次回予告
もうちょい複雑なのが作りたいです。