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

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

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

コロナ禍によって、オーリーズの働き方は一変しました。

例えばオフィス通勤だったのがほぼ完全にリモートワーク可能となり、それに対応して通信費やリモートワーク環境構築費(10万円!)の支給など充実した手当も頂いています。

しかし、働くうえで今までと大きく変わってしまったことがあります。

それは猫社員・まる美の存在です。

以前までオフィスを闊歩して(たいてい副社長・誠愛さんのデスクの近くにいましたが)社員に癒しを与えていたまる美ですが、「猫もコロナにかかる」ということで自宅待機となっています。(それってただの飼い猫では???

実際、入社の際に猫が決め手となった方もいるため、まる美が見られない環境というのは、著しく社員の幸福度が下がっている状態と言っても過言ではないでしょう。

※オフィスで働くまる美、リモートワークをするまる美の様子は上のリンクから

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

ということで、まる美に触れ合うことができないという問題を解決すべく「まる美bot」を作成することにしました。オーリーズの社内通知をSlack上のまる美からさせようという取り組みです。Slackとはいえ、まる美の方からコミュニケーションを取ってくれるなんて社員の幸福度が高まることは確実ですね。

まる美から話してもらいましょう。ボットにしゃべらせるテストの様子です。

そして、今回まる美botから社内に通知するのは「社内向けのナレッジ共有データベースのページ更新通知」にします。

オーリーズではNotionで社内ポータルを構築しているのですが、Notion⇔Slack間の連携機能を使うとNotionの変更すべてがSlackへと送信されてしまい、通知の量が膨大となってしまっています。

5分くらいでこのくらいの通知が流れます。本当に欲しい情報がどれかわからないですね。

本当は社内ポータルの中の社内向けのナレッジ共有データベース内ページのプロパティ変更のみをピックアップして送りたいのですが、Notionの設定ではそのような設定ができません。そのため簡単なプログラムを書いて、それをまる美botと連携させることで対応することにしました。

実装の流れ

上述した内容を実現するために必要なことは大きく分けると以下です。

  1. Slack用のまる美botを作成する。
  2. NotionのAPI実行用のインテグレーションを作成する。
  3. Notionの記事更新を発見するプログラムを作る。(記事には「執筆中」「完了」のステータスがあるため「完了」になったものを検知する。)
  4. 上のプログラムを定期的に実行する。もしくは記事が公開されたタイミングでプログラム実行を行う。
  5. 結果をまる美botから送信する。

1、2のSlackBotを作ることはそう難しくはありません。

3、4については、執筆中→完了というステータス変化をどのように検知するのかという点どこでプログラムを実行するのかという点が重要になってきます。

GAS(Google Apps Script)の活用

今回は以下の理由からGAS(Google Apps Script)で実行することにします。

  1. 実行環境を用意することなく1時間おきにスクリプト実行ができる。
  2. 記事のステータスを管理しておくためにスプレッドシートが利用できる。
  3. 他の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 = "

②では①で受信した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]
  })
}

③④のspreadsheetの情報の取得については、以下のスクリプトでスプレッドシートの情報を取得することができます。(announceメソッドに追記します。)

var articles_sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("hogehoge");
var articles_sheet = articles_sh.getRange("A2:G"+articles_sh.getLastRow()).getValues();

⑤でNotionからAPIで得られたデータとSpreadSheetの情報を比較するのですが、一度ステータスが完了になったページについては再公開された際に通知を行う必要はないので、完了になったページが執筆中に戻ったときにはSpreadSheet上のステータスを「執筆中(一度公開済み)」とすることで再通知を防ぐ仕組みを作ります。

データの更新はAPIで取得したデータで基本的に上書きするため、SpreadSheetの情報を全部消したうえでAPIで取得したデータを書き込むことで更新を行います。

ただし、上記のような「執筆中(一度公開済み)」のようなステータスを加えなければならないので、必要に応じてAPIで取得したデータを修正しておきます。

最終的にページを比較する上でのロジックは以下となりました。

  1. APIで取得したページのステータスが「執筆中」である際に、前回のステータスに「完了」もしくは「公開」の文字列が含まれている際は「執筆中(一度公開済み)」とする。
  2. 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);
    }
  })

⑥⑦シートの更新と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);
  })

こちらは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;
}

あとはこのスクリプトを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;
}

実際の通知と社員の反応

  • 実際の通知
  • 社員の反応

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

  • まる美の活躍領域を拡張した続編はこちら

オーリーズは、クライアントの「ビジネス目標達成」に伴走するマーケティングエージェンシーです。

「代理店の担当者が自社の業界・戦略に対する理解が不足しており、芯を食った提案が出てこない」
「新規の広告出稿に関する提案が中心で、最終的なビジネスゴールに紐づく本質的な提案がもらえない」
「広告アカウントが開示されないため、情報が不透明で自社にノウハウやナレッジが蓄積しない」

既存の広告代理店に対してこのようなお悩みをお持ちの場合は、一度オーリーズにお問い合わせください。

オーリーズの広告運用支援では、①運用者の担当社数の上限を4社までに制限②担当者のKPIは出稿金額ではなくNPS(顧客満足度)③アカウントは広告主が保有することを推奨 しており、目先のコンバージョン増加にとどまらず、深い事業理解を基にしたマーケティング戦略の立案や実行支援が可能です。

オーリーズのサービス資料をダウンロードする(無料)
オーリーズのコーポレートサイト
支援事例(クライアントの声)
オーリーズブログ

オーリーズへ問い合わせる

この記事を書いた人

株式会社オーリーズ

アシスタント・マネージャー

北原 直明

保険代理店向けの営業管理パッケージシステムの導入・開発に従事。開発責任者として開発基盤の刷新やAmazon Web Services基盤への移行プロジェクトのPMに従事。その後、自分のエンジニアリングスキルをマーケティング領域で活用すべくオーリーズに入社。広告運用を通じ、各種広告チャネルとマーケティングツールを基盤データ連携できるストラテジストとして邁進中。

最近書いた記事