こんにちは、エンジニアをしているタカです。
現在、主に自動テストツール関連や、新卒入社者向けの教育業務に携わっています。
本記事は、All-in-one workspaceとして、近年人気が高まってきているNotionのAPIを使った通知システムの構築方法を紹介します。

Notionとは

NotionはAll-in-one workspaceと謳われるツールです。
簡単なメモとして使うにとどまらず、タスク管理、PJ管理、ナレッジ管理など、様々な用途で使うことができます。
Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.
マークダウン記法が使えることからエンジニアもとっつきやすく、弊社でもNotionの活用事例が徐々に増えてきています。

Notion APIとは

そんな便利なNotionですが、APIも存在し、記事執筆時点(2022年春)で無料で使えます。
Start building with the Notion API
永らくBeta版という位置づけでしたが、2022年2月に晴れて正式版となりました。
現時点で、このAPIを用いて、以下のような操作が可能です。

  • データベースの新規作成
  • 指定のデータベースの取得、更新
  • 指定のデータベース内のページを条件を付けて検索
  • ページの新規作成、削除や指定のページの更新

データベースとは、Notionにおけるページの集合体です。
情報が載っているページを構造化し、Notionの画面上で様々な見せ方をすることが可能ですが、簡単に言うとページの親のようなイメージで、管理の単位になります。
APIからデータベース(DB)に対して行える操作は
このDBと、ページに対して行えるものが存在します。
行える操作はシステム開発におけるDBに対してのCRUDのようなもので、
APIごとにGET、POST、PATCHとタイプが異なります。
今回はいくつかのAPIを使った仕組みを作っていきたいと思います。

やりたいこと

さて、今回やりたいこと(要件)は下記の通りです。

  • 指定のNotion DBから、特定条件に合致するページIDの一覧を取得
  • 一覧からページのURLとタイトルを1つ抽出
  • 抽出したページ情報をSlackに通知する
  • 通知したページのプロパティを上書きする
  • 一連の処理を定期実行する

使用場面は様々ありますが、例えば、各自が期限までに作成するページのリマインドやナレッジDBのページをデイリーでランダムに紹介する..などに使えるかと思います。

実装手順

ここからは実際にNotion APIを動かし、その後実装を行います。

Notion APIの使用設定をする

まず、Notion APIを使うには以下のページの Getting started を順番に行っていきます。
Start building with the Notion API
Step1では、ワークスペースとの連携の設定を行い、シークレットを取得します。
Step2では、既にあるDBまたは新たにDBを作成して、URLに含まれるDatabase IDを取得します。
いずれもgifアニメーションで手順が示されているので、あまり迷うことなく設定と取得が出来るかと思います。

APIの動作確認を行う

まずは、手元のシェルのcurlコマンドで動作確認を行います。
やりたいことがAPIで実現できなかったら元も子もないので、一度挙動を確かめてからコードの実装に入りましょう。
Notion APIは、公式ドキュメント上で、database_idなどの変数を入力して、シェルを含めた様々な言語の実装例を出力可能です。このため、比較的容易にシェルでの動作確認を行うことが出来ます。
なお、ステータスコードが200と400のときのレスポンスも詳しく書かれているので、実装の際もこちらを参照しつつコードを書いていくことを推奨します。

DBのページ一覧を取得

まずは、指定のDBにアクセスして、ページIDの一覧を取得します。
Database向けにはいくつかAPIがありますが、今回はDatabaseに含まれるページ情報を抽出するので、 Query a databaseというAPIが良さそうです。
Start building with the Notion API
APIを叩く際にbodyに条件を渡せますが、まずは条件無しでページの一覧を取得してみます。
シークレットとDBをシェル変数に代入した状態にしたうえでcurlで通信を行います。

# シェル変数にシークレットを設定
$ NOTION_KEY=secret_xxxxxx
# シェル変数にDBのIDを設定
$ NOTION_DATABASE_ID=xxxxxx
# DB情報を取得するAPIを実行. ページ一覧のobjectが返ってくる
$ curl -X POST 'https://api.notion.com/v1/databases/'"$NOTION_DATABASE_ID"'/query' \
  -H 'Authorization: Bearer '"$NOTION_KEY" \
  -H 'Notion-Version: 2022-02-22' \
  -H "Content-Type: application/json";

# ヘッダーのNotion-VersionはAPIのリリースごとのバージョンで、
# 今回は記事執筆次点で最新(2022-02-22)を使用します。

次に、条件に合致するページ一覧を取得します。
今回のデータベースでは、ページが通知済か管理するため、プロパティにremindというチェックボックスを一つ準備するものとします。
条件には、このチェックボックスが入っていない(false)ことを指定します。
下記のコマンドで、条件に合致するページ一覧を取得できました。

# remindのチェックボックスが入っていないものを対象
$ curl -X POST 'https://api.notion.com/v1/databases/'"$NOTION_DATABASE_ID"'/query' \
  -H 'Authorization: Bearer '"$NOTION_KEY" \
  -H 'Notion-Version: 2022-02-22' \
  -H "Content-Type: application/json" \
  --data '{
    "filter": {
        "or": [
            {
                "property": "remind",
                "checkbox": {
                    "equals": false
                }
            }
          ]
      }
    }';

ページ更新

要件では、次に一覧からページを一つ抽出し、その後、通知を行います。
完了したら、通知したページのremindプロパティのチェックボックスをONにするので、
ページ更新のAPIを叩きます。
ここでのUpdate pageのAPIを用います。
Start building with the Notion API
remindというチェックボックスのプロパティをtrueにするので、少しjsonが入れ子になりますが、下記のコマンドでページを更新できます。

$ curl -X PATCH https://api.notion.com/v1/pages/{page_id} \
  -H 'Authorization: Bearer '"$NOTION_KEY"'' \
  -H "Content-Type: application/json" \
  -H "Notion-Version: 2022-02-22" \
  --data '{
  "properties": {
    "remind": { "checkbox": true }
  }
}'

# {page_id}には固定のページIDが入ります。
# 事前にremindというチェックボックスのプロパティが必要。
# ページ情報のobjectが返ってくる

実行後、NotionのPageを確認して問題なくチェックボックスが入っていることを確認します。
以上で、APIの動作確認が出来たため、実装に入っていきます。

コードを実装する(GAS)

ここからは、動作確認した内容をもとにコードを実装します。
今回、実行環境はGoogleAppScript(GAS)を使いたいと思います。
GASは無料でサーバレス環境で処理を動かすことができ、定期実行も容易にできるので、今回の目的にはぴったりと言えます。

シークレットをプロパティストアで管理する

GASで実装を行うにあたり、NotionのシークレットとDatabase id、通知用のSlackのWebhookのパスをコードで使用する必要があります。
運用にもよりますが、特にシークレットは公開するメンバを絞る必要があるので、コードに直接書くのではなく、専用のデータ格納領域であるプロパティストアで管理します。
Properties Service | Apps Script | Google Developers
管理方法は、functionにkeyとvalueの形式でプロパティを書いて実行するといったものになります。
※ functionを定義するGASファイル(.gsファイル)は、本処理とは別のもので構いません
Apps Script画面で下記のコードを書いて、保存・実行します。
一度実行することで、次回からはどの.gsファイルでもキー名を呼び出して使用できます。

function setUserProperty() {
  const userProperties = PropertiesService.getUserProperties();

  userProperties.setProperties({
    'NOTION_KEY': 'xxxx',
    'NOTION_DATABASE_ID': 'xxxx',
        'SLACK_WEBHOOK_PARAMETER': 'xxxx'
  });

    // 保存されたか確認
    const properties = userProperties.getProperties();
      for (let key in properties) {
        Logger.log('Key: %s, Value: %s', key, properties[key]);
      }
}

DBのページ一覧を取得

ここから各処理のコードを書いていきます。
まずは、特定条件に合致するDBのページ一覧を取得する部分です。

function fetchURL() {
  const requestUrl = "https://api.notion.com/v1/databases/" + userProperties.getProperty("NOTION_DATABASE_ID") + "/query";

  const headers = {
    "Content-Type" : "application/json",
    "Notion-Version" : "2022-02-22",
    "Authorization" : "Bearer " + userProperties.getProperty("NOTION_KEY"),
  };

    // remindプロパティのチェックが付いてないものを対象
  const payload = {
    "filter" : {
      "or" : [
        {
          "property" : "remind",
          "checkbox" : {
            "equals" : false
          }
        }
      ]
    }
  };

  const requestOptions = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(payload),
  }

    // APIに通信
  const response = UrlFetchApp.fetch(requestUrl, requestOptions);

  // resultsキーにページ情報のオブジェクトの配列が格納されている
  return JSON.parse(response.getContentText()).results;

プロパティストアに格納したシークレットとDB名を取得する必要がありますが、保存時にも使用したオブジェクトuserPropertiesのメソッドで値を取得します。
なお、APIへの通信部分の実装は、ドキュメントを参考にしています。
Class UrlFetchApp | Apps Script | Google Developers

PageのタイトルとURLを抽出する

次に、通知するページのタイトルとURLを抽出するコードを書きます。
fetchURL()の戻り値の配列を引数として受け取り、map()の反復処理で、ページのタイトルとid(ページID)を抽出します。
このidは、NotionのページURLのパス部分に含まれるユニークな英数字のことを指します。

function selectNotifyPage(notionRetrieveResults) {
  // pageごとのタイトルとURLを取得
  const pageTitleUrls = notionRetrieveResults.map(
    page => {
      return {
        "title" : page.properties.name.title[0].plain_text,
        "id" : page.id.replaceAll('-', '')
        }
      });
  
  // 通知ページを一覧の配列からランダムで決定
  return pageTitleUrls[Math.floor(Math.random() * pageTitleUrls.length)];

プロパティ値の部分は、引数のオブジェクトを確認したうえで実装しました。(いずれも記事執筆時点のNotionの仕様のようです。)

  • タイトルはオブジェクトの階層深くのplain_textプロパティの値
  • idプロパティは、デフォルトだとハイフンが複数含まれているので取り除く
  • オブジェクトのプロパティにURLという値があるが、Notionのページ名によっては、id部分に、前述のidとは別のハイフンと文字列が含まれるため、素直にidプロパティを使用

結果をslackのWebhookに送信する

function notifySlack(pageTitle, pageId) {
  const requestUrl = "https://hooks.slack.com/services/" + userProperties.getProperty("SLACK_WEBHOOK_PARAMETER");

  const headers = {
    "Accept" : "application/json",
  };

  const payload = {
    "text" : "好きな文字列を入れる" + pageTitle + "\nhttps://www.notion.so/" + pageId
  };  

  const requestOptions = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(payload)
  }

  return UrlFetchApp.fetch(requestUrl, requestOptions);
}

こちらは特に補足する内容は無いと思います。
予め、GASとSlackを連携させておきます。
Notionページプロパティの上書き処理

function updatePageStatusNotified(pageId) {
  const requestUrl = "https://api.notion.com/v1/pages/" + pageId;

  const headers = {
    "Accept" : "application/json",
    "Notion-Version" : "2022-02-22",
    "Authorization" : "Bearer " + userProperties.getProperty("NOTION_KEY"),
  };

  const payload = {
    "properties" : {
      "remind" : {
        "checkbox" : true
      }
    }
  };

  const requestOptions = {
    "method" : "patch",
    "headers" : headers,
    "payload" : JSON.stringify(payload)
  }

    return UrlFetchApp.fetch(requestUrl, requestOptions);
}

定期実行する

GASのトリガー設定で行います。
説明は割愛しますが、特定の時間などに実行するように設定すると良いかと思います。

実行結果

おわりに

Notionは、使えば使うほど新たな発見があり面白いサービスです。
更にAPIを活用することで、用途や可能性が広がっていくと思いますので、ぜひ皆さんも活用してみてください。

<おすすめ記事・メディア>

【Notion】SlackからNotionに直接タスク登録できるタスク管理データベースをつくる

SHARE

  • facebook
  • twitter

SQRIPTER

AGEST Engineers

AGEST

記事一覧

AGESTのエンジニアが情報発信してます!

株式会社AGEST

Sqriptsはシステム開発における品質(Quality)を中心に、エンジニアが”理解しやすい”Scriptに変換して情報発信するメディアです

  • 新規登録/ログイン
  • 株式会社AGEST