🤖

Notionで書籍管理できるDiscord botを開発した話

2022/04/17に公開

はじめに

高専生が参加するdiscordサーバにて、Notionで書籍管理ができるbotを開発したので、今回はそのbotの紹介をしていきます。

開発背景

作業するときなどに頻繁に使っているDiscordから、Notionに簡単に書籍情報を追加したいと思ったのがきっかけです。

僕は普段からNotionを使って備忘録を書いたり、いいなと思った記事を集めているのですが、書籍管理もNotionでいけるじゃん! と思いデータベースを使用して以下のような管理ページを作成してみました。

しかし、いちいち書影をアップロードしたりタイトルを入力したりと、書籍を追加する作業が結構面倒でした。そこで、Noiton APIを使用して書籍情報を追加・削除なんかができるbotがあれば便利だな〜と思い、開発することにしました。

機能

実装した以下の機能を紹介していきます。

  • 書籍検索
  • 書籍追加
  • 書籍取得
  • 書籍削除

今思うと削除機能はNotionでワンタップでできてしまうのでいらなかった気がします。。

書籍検索

書籍をタイトルで検索する機能です。
DiscordのSlash Commandで/searchに続いて任意のタイトルを入力するとそのタイトルであいまい検索してくれます。

デザインパターンで検索すると、ヒットした書籍のタイトルが5件返されます。
タイトルの下の数字がISBN値で書籍追加で使用します。

書籍追加

Notionに書籍を追加する機能で、追加にはISBN値を使用します。
/searchコマンドでタイトルの下にISBN値が返されるのでそれを入力するとNotionに追加されます。

登録書籍取得

fetchコマンドでNotionに登録されている書籍を一覧表示します。
タイトルの下にはNotionで割り振られるページIDが表示されていて、書籍を削除する際に使用します。

登録書籍削除

/deleteコマンドに続いて削除したい書籍のページIDを入力するとNotionから書籍が削除されます。

実装処理について

開発環境

  • macOS Monterey 12.2.1
  • node.js 16.6.1
  • discord.js 13.6.0

使用API

書籍検索

書籍検索にはGoogle Books APIsを使用しました。ISBN値からの書籍情報取得はopenBDだったのですが、タイトルで取得することができないようだったため、書籍検索と書籍追加で使用するAPIを分けました。

index.js
// searchコマンドが入力された時の処理
if (interaction.commandName === "search") {
    const userSearchText = interaction.options.getString('検索ワード')
    const encodeUrl = encodeURI(`${GOOLE_BOOKS_URL}${userSearchText}`)
    try {
	// 書籍情報取得
        const googleBooksRes = await axios.get(encodeUrl)
	
	// google books apiからの取得データを5件分にする
        const items = googleBooksRes.data.items.slice(0, 5)
	
	// fields配列に必要なデータを格納
        const fields = []
        items.map(item => {
            const title = item.volumeInfo.title
            const isbn = item.volumeInfo.industryIdentifiers[0].identifier
            const field = { name: title, value: `ISBN : ${isbn}` }
            fields.push(field)
        })

	// 結果を送信
        interaction.reply({
            embeds: [{
                title: `${userSearchText}」の検索結果`,
                fields: fields,
                color: 4303284,
                timestamp: new Date()
            }]
        })
    } catch (error) {
        console.log(error)
        interaction.reply('問題が発生しました。。')
    }
}

書籍追加

書籍追加にはopenBDを使用しました。

index.js
if (!message.author.bot && message.channel.id === SERVER_ID) {
    const channel = client.channels.cache.get(SERVER_ID)
    const userContent = message.content

    // ISBN値にマッチするか正規表現で判定
    if (ISBN_PATTERN.test(userContent)) {
        try {
            // 書籍情報を取得
            const openbdRes = await axios.get(`${OPENDB_URL}${userContent}`)
            const summary = openbdRes.data[0].summary
	    
	    // 書影がない場合はダミーの画像を使用
            const url = summary.cover || DUMY_URL

            // Notionに追加
            await axios.post(`${NOTION_URL}/pages`, createData(summary), headers)
            channel.send({
                content: 'Notionに追加されました!',
                embeds: [{
                    title: summary.title,
                    image: {
                        url: url
                    },
                    description: `ISBN : ***${summary.isbn}***`,
                    color: 4303284,
                    timestamp: new Date()
                }]
            })
        } catch (error) {
            console.log(error)
            channel.send('Notionへの追加が失敗しました。。。')
        }
    }
}

書籍取得

index.js
if (interaction.commandName === "fetch") {
  try {
      // Notionの書籍情報取得
      const notionRes = await axios.post(`${NOTION_URL}/databases/${DATABASE_ID}/query`, {}, headers)
      const results = notionRes.data.results
      
      // fieldsに各情報を格納
      const fields = []
      results.map(result => {
          const title = result.properties['タイトル'].title[0].text.content
          const pageId = result.id
          const field = { name: `:book: ${title}`, value: `ページID: ${pageId}` }
          fields.push(field)
      })
      
      // 結果を送信
      interaction.reply({
          embeds: [{
              title: 'Notionで管理している書籍一覧',
              fields: fields,
              color: 4303284,
              timestamp: new Date()
          }]
      })
  } catch (error) {
      console.log(error)
      interaction.reply('問題が発生しました。。')
  }
}

書籍削除

index.js
if (interaction.commandName === "delete") {
    const userDeletePageId = interaction.options.getString('ページ')
    try {
        // Notoinの書籍情報削除
        await axios.patch(`${NOTION_URL}/pages/${userDeletePageId}`, {archived: true}, headers)
        interaction.reply('ページが削除されました')
    } catch (error) {
        console.log(error)
        interaction.reply('問題が発生しました。。')
    }
}

ハマりポイント

openBDで書影がない書籍がある

書籍追加の際にユーザが入力したISBN値をopenBDのエンドポイントに使って書籍情報を取得していたのですが、書影のデータがない書籍があるために、Notionに追加できないエラーが発生。

下のcreateData関数ではopenBDで取得した情報をNotion APIで追加できるオブジェクトに変換しているのですが、"画像"プロパティのurlに入ってくるurlが空文字の場合Notionに追加されずエラーとなってしまいます。ここに気づかずに、ネット遅いなーとか思ってました、、

最終的にダミーの画像を用意して対策しました。

 const url = cover || DUMY_URL
const createData = (summary) => {
    const { title, cover, isbn } = summary
    
    // ダミーURLを入れて対策
    const url = cover || DUMY_URL

    const data = {
        parent: { database_id: DATABASE_ID },
        properties: {
            // ...その他プロパティ情報
            "画像": {
                "files": [
                    {
                        "type": "external",
                        "name": title,
                        "external": {
                            "url": url
                        }
                    }
                ]
            },
            // ...その他プロパティ情報
        }
    }

    return data
}

さいごに

自分で言うのもなんですが、割と便利なbotを開発できたなと思います。開発期間も1日程でしたので集中してスピード感を持って開発できました。今後は、Notionの書籍情報の更新や書籍追加時にデフォルト値を設定できる機能を追加していきたいと思います。

参考リンク

参考記事

こちらの記事を参考にさせていただきました!
ありがとうございました!
https://qiita.com/str32/items/39283355e71240113e09
https://qiita.com/yuto0214w/items/1ecee25efca6b5b7445b
https://qiita.com/InkoHX/items/590b5f15426a6e813e92
https://hetima333.hatenablog.com/entry/2018/10/13/204206

Discussion