8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-29

野望

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

前回までのあらすじ

LINEオウム返しBotを作ったり、Notion APIの検証をしたり。

今回やったこと

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

image.png

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

削除はまだできません。

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

手順

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

①Notionでテーブルの準備(ステータス管理テーブル編)

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

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

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

image.png

②Notionでテーブルの準備(データ本体用テーブル編)

過去回のテーブルをそのまま流用しています。
・項目1:key
・項目2:value
(・項目3:時刻 ←検索時に直近n個の値をとってくる使い方ができると思って用意した。今回は未使用。)

③リッチメニューの準備

LINE Official Manager > トークルーム管理 > リッチメニューにて簡単なメニューを用意します。
image.png

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

④Lambdaの処理ロジック検討

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

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

⑤実装

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

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

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

次回予告

もうちょい複雑なのが作りたいです。

8
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?