【Notion API】GASでプロパティの変更を検知してSlackに通知する仕組み

コロナ禍以後のオーリーズの働き方

コロナ禍によって、オーリーズの働き方は一変しました。
例えばオフィス通勤だったのがほぼ完全にリモートワーク可能となり、それに対応して通信費やリモートワーク環境構築費(10万円!)の支給など充実した手当も頂いています。
しかし、働くうえで今までと大きく変わってしまったことがあります。
それは猫社員・まる美の存在です。
 
以前までオフィスを闊歩して(たいてい副社長・誠愛さんのデスクの近くにいましたが)社員に癒しを与えていたまる美ですが、「猫もコロナにかかる」ということで自宅待機となっています。(それってただの飼い猫では???
実際、入社の際に猫が決め手となった方もいるため、まる美が見られない環境というのは、著しく社員の幸福度が下がっている状態と言っても過言ではないでしょう。
※オフィスで働くまる美、リモートワークをするまる美の様子は上のリンクから

Notionページのプロパティ変化のみをbotでSlackに通知する

ということで、まる美に触れ合うことができないという問題を解決すべく「まる美bot」を作成することにしました。オーリーズの社内通知をSlack上のまる美からさせようという取り組みです。Slackとはいえ、まる美の方からコミュニケーションを取ってくれるなんて社員の幸福度が高まることは確実ですね。
まる美から話してもらいましょう。ボットにしゃべらせるテストの様子です。
まる美から話してもらいましょう。ボットにしゃべらせるテストの様子です。
そして、今回まる美botから社内に通知するのは「社内向けのナレッジ共有データベースのページ更新通知」にします。
オーリーズではNotionで社内ポータルを構築しているのですが、Notion⇔Slack間の連携機能を使うとNotionの変更すべてがSlackへと送信されてしまい、通知の量が膨大となってしまっています。
5分くらいでこのくらいの通知が流れます。本当に欲しい情報がどれかわからないですね。
5分くらいでこのくらいの通知が流れます。本当に欲しい情報がどれかわからないですね。
本当は社内ポータルの中の社内向けのナレッジ共有データベース内ページのプロパティ変更のみをピックアップして送りたいのですが、Notionの設定ではそのような設定ができません。そのため簡単なプログラムを書いて、それをまる美botと連携させることで対応することにしました。

実装の流れ

上述した内容を実現するために必要なことは大きく分けると以下です。
  1. Slack用のまる美botを作成する。
  1. NotionのAPI実行用のインテグレーションを作成する。
  1. Notionの記事更新を発見するプログラムを作る。(記事には「執筆中」「完了」のステータスがあるため「完了」になったものを検知する。)
  1. 上のプログラムを定期的に実行する。もしくは記事が公開されたタイミングでプログラム実行を行う。
  1. 結果をまる美botから送信する。
1、2のSlackBotを作ることはそう難しくはありません。
3、4については、執筆中→完了というステータス変化をどのように検知するのかという点どこでプログラムを実行するのかという点が重要になってきます。

GAS(Google Apps Script)の活用

今回は以下の理由からGAS(Google Apps Script)で実行することにします。
  1. 実行環境を用意することなく1時間おきにスクリプト実行ができる。
  1. 記事のステータスを管理しておくためにスプレッドシートが利用できる。
  1. 他のGoogleサービスとの連携が容易である。
記事を完成してすぐに通知を送らずとも、多少のタイムラグがあって通知が来ても問題ないでしょう(1時間おきの巡回で十分)。
また、「執筆中」⇒「完了」のステータス変化を捕捉するために前回の実行結果をスプレッドシートに保存しておけるのは理想的な環境です。Google連携サービスは今後のまる美botの機能追加で利用します。

具体的な実装

では、具体的な実装を見てみましょう。
※今回の記事ではSlackアプリの作成やNotionAPI用のインテグレーションの設定についての説明は行いません。(探せばすぐに出てくると思いますので、認証用のトークンは取得できている前提で話します。)
①のNotionAPIを実行については、NotionのAPIを用いてデータベースで管理されているページ一覧の情報を取得しましょう。
getNotionDataメソッド内のアクセストークンを設定し、データベースidを引数に渡せばデータベースの情報をjson形式で取得することができます。
(トークンはPropertiesService.getScriptProperties().getProperty("hogehoge")などで実装してもらえると理想的だと思います。)
function announce(){
	var article_data = getNotionData("xxxxxxxxxxxxxxxxxxxxxxxxxxxx").results
}
//notionのデータベースのデータを取ってくるメソッド
function getNotionData(database_id){
	var access_token = "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
	var headers = {
		"Content-Type": "application/json",
		"Authorization": "Bearer " + access_token,
		"Notion-Version": "2022-02-22",
	};
	var options = {
		"method" : "post",
		"headers" : headers,
	};
	var url = "https://api.notion.com/v1/databases/" + database_id + "/query";
	var response = UrlFetchApp.fetch(url,options);
	notion_data = JSON.parse(response);
	return notion_data;
}
JavaScript
②では①で受信したjsonの情報を整形します。③④で得られる前回実行時のデータ(SpreadSheet)の情報との比較となるので、必要な項目のみを配列形式で保持しましょう。
announceメソッド内に追記します。
function announce(){
	var article_data = getNotionData("xxxxxxxxxxxxxxxxxxxxx").results
	var articles_api = article_data.map(function(value){
	  var id = value.id.replaceAll("-","");
	  var status = ""
	  if(value.properties.ステータス.select !== null)status = value.properties.ステータス.select.name;
    var created_time = value.properties['作成日(自動)'].created_time;
    var last_edited_time = value.properties['更新日(自動)'].last_edited_time;
    var title = "";
    value.properties.名前.title.map(element => title += element.plain_text);
    var created_by = value.properties['作成者(自動)'].created_by.name;
    var avatar_url = value.properties['作成者(自動)'].created_by.avatar_url;
    return[id,status,created_time,last_edited_time,title,created_by,avatar_url]
  })
}
JavaScript
③④のspreadsheetの情報の取得については、以下のスクリプトでスプレッドシートの情報を取得することができます。(announceメソッドに追記します。)
var articles_sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("hogehoge");
var articles_sheet = articles_sh.getRange("A2:G"+articles_sh.getLastRow()).getValues();
JavaScript
⑤でNotionからAPIで得られたデータとSpreadSheetの情報を比較するのですが、一度ステータスが完了になったページについては再公開された際に通知を行う必要はないので、完了になったページが執筆中に戻ったときにはSpreadSheet上のステータスを「執筆中(一度公開済み)」とすることで再通知を防ぐ仕組みを作ります。
データの更新はAPIで取得したデータで基本的に上書きするため、SpreadSheetの情報を全部消したうえでAPIで取得したデータを書き込むことで更新を行います。
ただし、上記のような「執筆中(一度公開済み)」のようなステータスを加えなければならないので、必要に応じてAPIで取得したデータを修正しておきます。
最終的にページを比較する上でのロジックは以下となりました。
  1. APIで取得したページのステータスが「執筆中」である際に、前回のステータスに「完了」もしくは「公開」の文字列が含まれている際は「執筆中(一度公開済み)」とする。
  1. APIで取得したページのステータスが「完了」である際に、前回の実行結果が「執筆中」もしくは「ページがない」状態であれば、後の処理で利用する通知用の配列(toAnnounce)に追加する。
(以下をannounceメソッドに追記します。)
 
var toAnnounce = [];
  articles_api.map(function(value){
    if(value[1].indexOf('公開') == -1 && value[1].indexOf('完了') == -1){
      var result = articles_sheet.some(function(data){
        return data[0] == value[0] && (data[1].indexOf('公開') != -1 || data[1].indexOf('完了') != -1)
      })
      if(result == true)value[1]="執筆中(一度公開済み)"
    }
    if(value[1].indexOf('公開') != -1 || value[1].indexOf('完了') != -1){
      var result = articles_sheet.some(function(data){
        return data[0] == value[0] && (data[1].indexOf('公開') != -1 || data[1].indexOf('完了') != -1)
      })
      if(result == false)toAnnounce.push(value);
    }
  })
JavaScript
⑥⑦シートの更新とslackへの通知を行います。(以下をannounceメソッドに追記します。)
データは全部上書きなので一旦2行目以降をすべてクリアしてから貼り付けを行います。
clearSheet("hogehoge")
articles_sh.getRange("A2:G"+(articles_api.length+1)).setValues(articles_api);
toAnnounce.map(function(value){
    var message = value[5] + "さんの記事が内部公開されたにゃ:clap:\n"+value[4]+"\nhttps://www.notion.so/" + value[0]
    callSlackApiMessage(message);
  })
JavaScript
こちらはannounceメソッド外に加えます。
//シートをクリアするためのメソッド
function clearSheet(sheetName){
    var workSh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    if(workSh.getLastRow() > 1){
    workSh.getRange(2,1,workSh.getLastRow()-1,workSh.getLastColumn()-2).clear();
    }
}
//Slack通知用のメソッド
function callSlackApiMessage(message){
  const response = UrlFetchApp.fetch(
    `https://www.slack.com/api/chat.postMessage`,
    {
      method: "post",
      contentType: "application/x-www-form-urlencoded",
      headers: { "Authorization": `Bearer xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` },
      payload: {
        channel: "xxxxxxxxxxxxxxx",
        text: message
      }
    }
  );
  console.log(`response: ${response}`)
  return response;
}
JavaScript
あとはこのスクリプトをGASのトリガー設定で一時間おきに実行するだけです!
  • スクリプト全体
    • function announce(){
      	var article_data = getNotionData("xxxxxxxxxxxxxxxxxxxxxxxxxxxx").results
      	var articles_api = article_data.map(function(value){
      	  var id = value.id.replaceAll("-","");
      	  var status = ""
      	  if(value.properties.ステータス.select !== null)status = value.properties.ステータス.select.name;
          var created_time = value.properties['作成日(自動)'].created_time;
          var last_edited_time = value.properties['更新日(自動)'].last_edited_time;
          var title = "";
          value.properties.名前.title.map(element => title += element.plain_text);
          var created_by = value.properties['作成者(自動)'].created_by.name;
          var avatar_url = value.properties['作成者(自動)'].created_by.avatar_url;
          return[id,status,created_time,last_edited_time,title,created_by,avatar_url]
        })
        var articles_sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("hogehoge");
        var articles_sheet = articles_sh.getRange("A2:G"+articles_sh.getLastRow()).getValues();
        var toAnnounce = [];
        articles_api.map(function(value){
          if(value[1].indexOf('公開') == -1 && value[1].indexOf('完了') == -1){
            var result = articles_sheet.some(function(data){
              return data[0] == value[0] && (data[1].indexOf('公開') != -1 || data[1].indexOf('完了') != -1)
            })
            if(result == true)value[1]="執筆中(一度公開済み)"
          }
          if(value[1].indexOf('公開') != -1 || value[1].indexOf('完了') != -1){
            var result = articles_sheet.some(function(data){
              return data[0] == value[0] && (data[1].indexOf('公開') != -1 || data[1].indexOf('完了') != -1)
            })
            if(result == false)toAnnounce.push(value);
          }
        })
        clearSheet("hogehoge")
        articles_sh.getRange("A2:G"+(articles_api.length+1)).setValues(articles_api);
        toAnnounce.map(function(value){
          var message = value[5] + "さんの記事が内部公開されたにゃ:clap:\n"+value[4]+"\nhttps://www.notion.so/" + value[0]
          callSlackApiMessage(message);
        })
      }
      //notionのデータベースのデータを取ってくるメソッド
      function getNotionData(database_id){
      	var access_token = "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      	var headers = {
      		"Content-Type": "application/json",
      		"Authorization": "Bearer " + access_token,
      		"Notion-Version": "2022-02-22",
      	};
      	var options = {
      		"method" : "post",
      		"headers" : headers,
      	};
      	var url = "https://api.notion.com/v1/databases/" + database_id + "/query";
      	var response = UrlFetchApp.fetch(url,options);
      	notion_data = JSON.parse(response);
      	return notion_data;
      }
      //シートをクリアするためのメソッド
      function clearSheet(sheetName){
          var workSh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
          if(workSh.getLastRow() > 1){
          workSh.getRange(2,1,workSh.getLastRow()-1,workSh.getLastColumn()-2).clear();
          }
      }
      //Slack通知用のメソッド
      function callSlackApiMessage(message){
        const response = UrlFetchApp.fetch(
          `https://www.slack.com/api/chat.postMessage`,
          {
            method: "post",
            contentType: "application/x-www-form-urlencoded",
            headers: { "Authorization": `Bearer xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` },
            payload: {
              channel: "xxxxxxxxxxxxxxx",
              text: message
            }
          }
        );
        console.log(`response: ${response}`)
        return response;
      }
      JavaScript

実際の通知と社員の反応

  • 実際の通知
  • 社員の反応
Notionの変更通知をSlackに送信したいものの、Notionの機能だけではかゆい所に手が届かず困っておられる方も多いのではないでしょうか。今回は弊社で実装した実例を紹介しましたが、この内容が参考になれば幸いです。
  • まる美の活躍領域を拡張した続編はこちら

\ SHARE ON /