🧭

Notionの更新をGASでDiscordに半自動で通知する

2022/12/11に公開
3

はじめに

こんにちは。Suzuhikiです。
VRゲームの開発をしながらWeb開発の勉強をがんばっています。

最近、チーム開発で情報をまとめるためにNotionを使い始めたのですが、メインのコミュニケーションツールとして利用しているDiscordに通知する方法が簡単に見つかりませんでした。
GithubやScrapboxには、リンクをコピーして貼り付けるだけで通知を飛ばすことができるWebhookのサービスがありましたが、Notionにはまだないようです。

Notion Platform Roadmapに記載があります。

Webhooks: We’re making investments in our infrastructure that will allow us to provide reliable event notifications.

試行錯誤の結果、GASを用いて半自動でNotionの更新情報をDiscordに通知する方法を見つけたのでまとめておきます。

概要

今回作成したシステムはNotionのデータベースの更新を監視することで動作します。
Notionのデータベースは様々な用途で利用できる少々複雑なものですので、一度Notion公式のデータベースの説明に目を通していただくとわかりやすいと思います。

実際に利用するNotionデータベースの構成を画像に示します。

先ほど半自動で通知すると表現したのは、このNotionデータベースに更新内容とページ名、編集者名を入力しないと通知できないからです。

このような回りくどい方法を採用したのには理由があります。

Notionではデータベースに行を追加すると自動的に「データベースページ」が生成され、データベースのカラムがパラメータとしてページの中に埋め込まれます。
下の画像がデータベースページの例です。

NotionAPIでは、新しく生成された通常ページを追跡するのが困難です。
通常ページの最終更新時間などの情報は、ページIDを特定した上でNotionAPIに問い合わせる必要がありますが、新しく作成された通常ページのページIDをNotionAPIから取得する方法はおそらくありません。

一方で、データベース内に生成されるデータベースページであれば追跡が可能です。
そこで、データベースページ内に、更新した通常ページの名前や、リンク(@をつけてメンションの対象に通常ページを指定することができます)などを記載することで、Discordに更新した通常ページについての情報を送信できるのではないかと考え実装しました。

GASの大まかな処理の流れは次のようになります。

  1. Notionデータベースの「通知済み」カラムがチェックされていない行をフィルターして取得する。
  2. 取得したすべての行にからDiscordに通知投稿を作成するのに必要な情報を抜き取る。
  3. 通知投稿を作成したNotionデータベースの行の「通知済み」のカラムにチェックを入れる。
  4. Discordに通知投稿を送る

提供されているNotionAPIの詳細は以下のドキュメントを参照してください。
https://developers.notion.com/reference/intro

前準備

NotionのAPIとDiscordのWebhookとGASを利用するためいくつか準備が必要です。

NotionAPIの準備

NotionのAPIを利用するにはIntegrationを作成して対象のNotionワークスペースに登録する必要があります。
Integrationはmy-integrationsのページから作成できます。
https://www.notion.so/my-integrations

Capabilitiesの項目は、コンテンツのReadとUpdate、ユーザー情報の取得を選択しておきます。

次にワークスペースに関連付けます。
対象のワークスペースを開いて、右上の3つのドットをクリックして出てくるメニューから、「コネクト」の項目に先ほど作成したIntegrationを登録してください。

DiscordのWebhookの準備

通知したいチャンネルにWebhookを追加します。

  1. Discordのチャンネル一覧から、通知したいDiscordチャンネルの⚙マーク(チャンネルの編集)をクリックします。
  2. 連携サービス > ウェブフックの順に選択し、「新しいウェブフック」ボタンを押します。
  3. お好みで作成されたウェブフックにアイコンや名前を付けます。

GASの準備

GAS(Google Apps Script)はGoogleDrive上にコードを配置して実行できるサービスです。
GoogleDriveのお好みの階層で右クリックし、その他 > Google Apps Script から新しいスクリプトを作成します。

Notionのデータベースの準備

NotionAPIで監視するデータベースを作成します。
データベースに必要なカラムを表に示します。

カラム名 データの種類 備考
更新内容 タイトル データベースページの名前になります。ここに変更内容をメモします。
ページ名 テキスト 実際に編集した通常ページへのリンクを@を使ってメンションすることで記載します。
編集者 ユーザー 編集した人を選択します。複数人でも動くはずです。
更新日時 最終更新日時 最終的にシステム側では使わなくなりました。どれが新しい更新か知るために表示しておくとよいと思います。
通知済み チェックボックス Discordの通知を送った後、システムが自動でチェクを付けます。人が操作する必要はありませんので、ビューで隠すなどしてください。

(表の要素の幅調整どうにかならないものですかね)

下の画像のような見た目になります。

実装

APIを確認しながらコードを眺めていただいた方がわかりやすいと思いますので、詳細な説明は控えておきます。
各関数には簡単な機能の説明を加えてあるので参考にしてください。
<<<>>>となっている部分はいままで用意したURLなどを挿入する箇所です。

コード全文
// GASのトリガーで一定間隔で実行する関数
function myFunction() {
  const TOKEN = "<<<Notion Integration Token>>>";

  const data = getNotionData(TOKEN);
  let fields = [];

  if(data == ""){
    return;
  }

  Logger.log(data)

  for(let page of data){
    const field = buildEmbedField(page.properties);
    fields.push(field);
    updateNotionPageStatus(page.id, TOKEN);
  }

  const embeds = buildEmbeds(fields);
  sendDiscordWebhook(embeds);
}

// Notionから更新情報を記載したデータベースの内容を取得する関数
function getNotionData(token) {
  const DATABASE_ID = "<<<監視対象のデータベースのID>>>";
  const URL = "https://api.notion.com/v1/databases/" + DATABASE_ID + "/query";
 
  let headers = {
    "content-type": "application/json",
    "Authorization": "Bearer " + token,
    "Notion-Version": "2022-06-28",
  };

  let filter = {
    "filter":{
      "and": [
        {
          "property": "通知済み",
          "checkbox":{
            "equals": false
          }
        },
        {
          "property": "更新内容",
          "title":{
            "is_not_empty": true
          }
        },
        {
          "property": "ページ名",
          "rich_text":{
            "is_not_empty": true
          }
        },
        {
          "property": "編集者",
          "people":{
            "is_not_empty": true
          }
        },
      ]
    },
    "sorts": [
      {
        "timestamp": "last_edited_time",
        "direction": "ascending"
      }
    ]
  }
 
  let options = {
    "method": "post",
    "headers": headers,
    "payload" : JSON.stringify(filter),
    "muteHttpExceptions": true,
  };

  Logger.log(options);
 
  let notion_data = UrlFetchApp.fetch(URL, options);
  notion_data = JSON.parse(notion_data);
 
  return notion_data["results"];
}

// DiscordのEmbedのFieldを構築する関数 今回は各Fieldに通常ページ1ページ分の情報を記載する
function buildEmbedField(page_data){
  const {page_name, page_url} = searchPageNameAndURL(page_data["ページ名"]["rich_text"]);
  const edit_log = page_data["更新内容"]["title"][0]["plain_text"];
  const editor = searchEditors(page_data["編集者"]["people"]);

  let field = {
    "name": page_name,
    "value": ">>> " + edit_log +"\nリンク:" 
      + page_url +"\n編集者:" + editor,
  };

  return field;
}

// Notionのデータベースの行のうち、Discordに通知したものには「通知済み」のカラムにチェックを入れる
function updateNotionPageStatus(id, token){
  const URL = "https://api.notion.com/v1/pages/" + id + "";
 
  let headers = {
    "content-type": "application/json",
    "Authorization": "Bearer " + token,
    "Notion-Version": "2022-06-28",
  };

  let payload = {
    "properties" : {"通知済み":{"checkbox": true}}
  }

  let options = {
    "method": "patch",
    "headers": headers,
    "payload" : JSON.stringify(payload),
    "muteHttpExceptions": true,
  };

  UrlFetchApp.fetch(URL, options);
}

// Embedsを作成する
function buildEmbeds(fields){
  const embeds = [
    {
      "type": "rich",
      "title": "Notionが更新されました!",
      "color": 0x00FFFF,
      "fields": fields,
      "url": "<<<NotionのトップページのURL(お好みで)>>>"
    }
  ]
  return embeds;
}

// EmbedsをDiscordのWebhookに送信する
function sendDiscordWebhook(embeds){
  const webhookUrl = "<<<DiscordのWebhookURL>>>";
  const jsonData = {
      "embeds": embeds
  };
  const payload = JSON.stringify(jsonData);
  const options = {
      "method": "post",
      "contentType": "application/json",
      "payload": payload
  };

  UrlFetchApp.fetch(webhookUrl, options);
}

// rich_textsは配列になっているため、更新した通常ページへのリンクが入っている要素を探す
function searchPageNameAndURL(rich_texts){
  let name = "";
  let url = "";

  for(let text of rich_texts){
    if(text["type"] == "mention"){
      name = text["plain_text"];
      url = text["href"];
      return {page_name: name, page_url: url};
    }
  }
  return {page_name: name, page_url: url};
}

// 編集者の配列であるpeoplesから全員の名前を取得する
function searchEditors(peoples){
  let editors = []

  for(people of peoples){
    editors.push(people["name"]);
  }

  return editors;
}

Embedsについて

Discordへの投稿をリッチに表示できるEmbedsを利用しています。
ドキュメントと、Embeds表示をプレビューできるサイトへのリンクを貼っておきます。
余裕があればお好みの表示に変えるのもいいかもしれません。
https://discordjs.guide/popular-topics/embeds.html#using-an-embed-object
https://autocode.com/tools/discord/embed-builder/
今回のシステムでは下の画像のような投稿が生成されます。

使い方

GASのトリガー設定

定期的にNotionのデータベースに変更がないか確認するために、GASのトリガーで一定間隔でmyFunction()関数を実行するように登録します。

  1. GASの画面右上の「デプロイ」ボタンから新しいデプロイを作成します。
  2. 画面左の「⏰トリガー」の項目からトリガー画面を開き、右下の「トリガーを追加」ボタンから新しいトリガーを作成します。
  3. 実行間隔を設定して保存します。私は5分おきに実行するように設定しています。

Notionを編集した時にすること

Notionの通常ページを更新した際にはGASが監視しているデータベースに行を追加する必要があります。
私(Suzuhiki)が、「仕様書」という名前の通常ページに、「VRゲームの自由度についての考えを追記」という編集をしたとすると、以下のように各カラムに入力することになります。

  1. 「更新内容」のカラムに「VRゲームの自由度についての考えを追記」と入力
  2. 「ページ名」のカラムで半角スペースを入力したあと、@マークを入力し、「仕様書」というページを探してメンションする。
  3. 「編集者」のカラムの選択肢から「Suzuhiki」を選ぶ
  4. 少し(最長5分)待つ
  5. Discordに通知が飛んでくる
  6. 「通知済み」のカラムに自動的にチェックが入っている

おわりに

今回はNotionの更新通知をDiscordに飛ばす方法を探してみたという話でした。
NotionAPIがデータベース中心に回っている感じがしていろいろいじっていたらうまくいきました。
チーム開発でメインのコミュニケーションサービスに通知を飛ばすのは重要だと思うので、どなたかのお役に立てれば幸いです。

現在学生中心のチームでVRソードアクションゲーム「Trace of Gladius」を開発していますので、宣伝がてらリンクを貼り付けておきます。
https://game-creators.camp/games/87789786/TraceOfGladius

それではよいお年を!

GitHubで編集を提案

Discussion

SieuSieu

指示に従いましたが、sendDiscordWebhook ステップに到達したときにエラーが発生しました。
例外:
https://discord.com のリクエストが失敗し、コード 400 が返されました。切り捨てられたサーバー応答: {"embeds": ["0"]} (完全な応答を調べるには、muteHttpExceptions オプションを使用します)
手伝って頂けますか ?

すずひきすずひき

コメントありがとうございます。

以下のサイトを参考に、"muteHttpExceptions" : trueを挿入してエラーを把握すると解決がしやすいと思います。
https://qiita.com/kunihiros/items/255070ba950a7ba95ae4

また、embeds周りの構築がうまくいっていない可能性があるので、sendDiscordWebhookの実行時点でのembeds変数の中身を確認してみてください。embedsを利用せず、より単純な"content"パラメータに変更して送信できるか確かめてみるのも良いと思います。

  const jsonData = {
-      "embeds": embeds
+      "content": "Hello from Webhook"
  };

お力になれると幸いです。

SieuSieu

ありがとう、情報を discord に送信しましたが、新しいタスクを作成して discord に送信したときと比較してどのタスクが変更されたかを知る方法はありますか?