LINE×Lambda×Notionで簡単なメモツール（追加・検索機能）

# 野望
DBにNotionを用いたLINE Bot的なサムシングを作りたいと思い続けています。

# 前回までのあらすじ
LINEオウム返しBotを作ったり、Notion APIの検証をしたり。

# 今回やったこと
LINEのリッチメニューを使って追加か検索を選択し、以下の操作ができるようになりました。
・NotionにKeyとValueの組を追加する
・Notionに登録されたKeyとValueを検索する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455207/8b146c9a-ba4a-5981-55df-eb6d4b8ca0b6.png)


また、今回はLINEでラリーを行いながら操作をするのでステータス管理も少しだけ行っています。
・追加時：「追加」選択→追加するデータを送信→追加完了
・検索時：「検索」選択→検索するキーワードを送信→検索結果返却

削除はまだできません。

過去の記事でLINEアカウント作成方法やAWSにおけるLambdaとAPI Gatewayの設定は書いていますので今回は割愛します。

# 手順
①Notionでテーブルの準備（ステータス管理テーブル編）
②Notionでテーブルの準備（データ本体用テーブル編）
③リッチメニューの準備
④Lambdaの処理ロジック検討
⑤実装

# ①Notionでテーブルの準備（ステータス管理テーブル編）
今回は以下のようにステータス管理のテーブルを作成して、最後に行った処理に応じて次の操作を制御しやすくしています。

・項目1:ステータス（ready/add/search）
・項目2:時刻

今回は一人で使う想定なのでこれだけの項目ですが、もっと複雑にしてもやりようはありそうです。
ex)複数人で使う場合はLINEのユーザID用の項目を用意する。
　　より複雑なラリーを行う場合はjsonなどのデータを保管する項目を用意する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455207/29ea96b2-d420-18a3-d138-905a09ffbb9a.png)

# ②Notionでテーブルの準備（データ本体用テーブル編）
過去回のテーブルをそのまま流用しています。
・項目1：key
・項目2：value
(・項目3：時刻　←検索時に直近n個の値をとってくる使い方ができると思って用意した。今回は未使用。)

# ③リッチメニューの準備
LINE Official Manager > トークルーム管理 > リッチメニューにて簡単なメニューを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455207/28a4ea24-119a-0d70-eca5-01b9c7d726ab.png)

今回は画像を用意せずに提供されているテンプレートを使用しています。
アクションはテキストを選択しています。
（メニューを選択したことによって追加・検索の処理がトリガーされるというよりも、メニュー選択によって投稿されたテキストによって各処理が始まります。）

# ④Lambdaの処理ロジック検討
追加・検索処理の一連の流れは次のようになります。
・追加：追加メニューで追加フロー開始→キーと値の要求→キー・値投稿→登録完了
・検索：検索メニューで検索フロー開始→検索文字列の要求→検索文字列投稿→検索結果返却

上記の流れに加えて、①で記載した通り、想定外の操作がされた時に備えて以下のステータス管理をしています。
・add:追加フロー開始時
・search:検索フロー開始時
・ready:追加or検索フロー完了時、フロー中に想定外の操作が起こり処理を中断した時

# ⑤実装
NotionのAPI検証を行った際の記事は↓です。
https://qiita.com/DJROU/items/b7e159cbbecf93678df5

相変わらずPromiseやらasync/awaitが苦手です。

```javascript:index.js
'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;
}
```

# できあがり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455207/ca8cce4b-f156-88fe-363d-c2fcc3b6723f.png)


# 次回予告
もうちょい複雑なのが作りたいです。