Notion - Updateログを適当にまとめてSlackに通知する
TL;DR
- Notion の All Updates は細かすぎて見るに堪えないので、適当にまとめてSlackに通知する方法
- SlackにすべてのUpdateを通知する
- Google Apps Script (GAS) でSlackのログを取得して、メッセージの内容からPage単位にまとめてSlackに通知し直す

目次
手順
SlackにすべてのUpdateを通知する
Updateの通知をしたいページの右上メニューから、Slackに接続する。

大量の通知が届くので専用の一時チャンネルを作るのがおすすめ。

NotionでSlackに接続するとサブページにも適用される。 トップノードの全ページに設定しておくとAll Updatesと同じメッセージが届くはず。
Notionの設定は以上。
SlackでOAuth Token取得
こちらの「SlackでOAuth Token取得」を参考にPermissionの設定まで行う。
次にこちらの「Slackアプリのインストール」までを参考にインストールを行う。
生成されたUser OAuth Tokenと、Bot User OAuth Tokenをコピーしておく。

GASの実装
スクリプトも先の2つのページから流用させて頂いた。感謝。
GASで新しいプロジェクトを作成してスクリプトをコピペする。
/*
Copyright (c) 2020 ryota-mo
Released under the MIT license
https://github.com/YukinobuKurata/YouTubeMagicBuyButton/blob/master/MIT-LICENSE.txt
*/
function ChannelLogToSlack() {
SetProperties();
const LOG_CHANNEL_NAME = "notion-updates";
const NOTIFY_CHANNEL_NAME = "notion-updates";
const MOST_RECENT_HOURS = 6;
const API_TOKEN = PropertiesService.getScriptProperties().getProperty('slack_api_token');
if (!API_TOKEN) {
throw 'You should set "slack_api_token" property from [File] > [Project properties] > [Script properties]';
}
const API_BOT_TOKEN = PropertiesService.getScriptProperties().getProperty('slack_api_bot_token');
if (!API_BOT_TOKEN) {
throw 'You should set "slack_api_bot_token" property from [File] > [Project properties] > [Script properties]';
}
let token = API_TOKEN;
let botToken = API_BOT_TOKEN;
let slack = new SlackAccessor(token, botToken);
let timestamp = parseInt(new Date() / 1000) - (60 * 60 * MOST_RECENT_HOURS);
// メンバーリスト取得
const memberList = slack.requestMemberList();
// チャンネル情報取得
const channelInfo = slack.requestChannelInfo();
// チャンネルにメッセージ内容を取得
for (let ch of channelInfo) {
if (ch.name == CHANNEL_NAME) {
console.log(ch.name)
let messages = slack.requestMessages(ch, timestamp);
PostLogSummary(slack, NOTIFY_CHANNEL_NAME, messages, memberList);
}
};
}
function SetProperties() {
PropertiesService.getScriptProperties().setProperty('slack_api_token', 'XXXXX');
PropertiesService.getScriptProperties().setProperty('slack_api_bot_token', 'XXXXX');
}
// Slack テキスト整形
function UnescapeMessageText(text, memberList) {
return (text || '')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/<@(.+?)>/g, function ($0, userID) {
var name = memberList[userID];
return name ? "@" + name : $0;
});
};
function RegExpMessage(text) {
//var pattern = /(?:edited|deleted|created).*(https.+)\?.*/g;
var pattern = /https[^?]+/g;
var result = text.match(pattern);
return result;
}
// Slack へのアクセサ
var SlackAccessor = (function () {
function SlackAccessor(apiToken, apiBotToken) {
this.APIToken = apiToken;
this.APIBotToken = apiBotToken;
}
var MAX_HISTORY_PAGINATION = 10;
var HISTORY_COUNT_PER_PAGE = 1000;
var p = SlackAccessor.prototype;
// API リクエスト
p.requestAPI = function (path, params) {
if (params === void 0) { params = {}; }
var url = "https://slack.com/api/" + path + "?";
// var qparams = [("token=" + encodeURIComponent(this.APIToken))];
var qparams = [];
for (var k in params) {
qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
}
url += qparams.join('&');
var headers = {
'Authorization': 'Bearer ' + this.APIToken
};
console.log("==> GET " + url);
var options = {
'headers': headers, // 上で作成されたアクセストークンを含むヘッダ情報が入ります
};
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response.getContentText());
if (data.error) {
console.log(data);
console.log(params);
throw "GET " + path + ": " + data.error;
}
return data;
};
// メンバーリスト取得
p.requestMemberList = function () {
var response = this.requestAPI('users.list');
var memberNames = {};
response.members.forEach(function (member) {
memberNames[member.id] = member.name;
console.log("memberNames[" + member.id + "] = " + member.name);
});
return memberNames;
};
// チャンネル情報取得
p.requestChannelInfo = function () {
var response = this.requestAPI('conversations.list');
response.channels.forEach(function (channel) {
console.log("channel(id:" + channel.id + ") = " + channel.name);
});
return response.channels;
};
// 特定チャンネルのメッセージ取得
p.requestMessages = function (channel, oldest) {
var _this = this;
if (oldest === void 0) { oldest = '1'; }
var messages = [];
var options = {};
options['oldest'] = oldest;
options['count'] = HISTORY_COUNT_PER_PAGE;
options['channel'] = channel.id;
var loadChannelHistory = function (oldest) {
if (oldest) {
options['oldest'] = oldest;
}
var response = _this.requestAPI('conversations.history', options);
messages = response.messages.concat(messages);
return response;
};
var resp = loadChannelHistory();
var page = 1;
while (resp.has_more && page <= MAX_HISTORY_PAGINATION) {
resp = loadChannelHistory(resp.messages[0].ts);
page++;
}
console.log("channel(id:" + channel.id + ") = " + channel.name + " => loaded messages.");
// 最新レコードを一番下にする
return messages.reverse();
};
// 特定チャンネルの特定のスレッドのメッセージ取得
p.requestThreadMessages = function (channel, ts_array, oldest) {
var all_messages = [];
let _this = this;
var loadThreadHistory = function (options, oldest) {
if (oldest) {
options['oldest'] = oldest;
}
Utilities.sleep(1250);
var response = _this.requestAPI('conversations.replies', options);
return response;
};
ts_array = ts_array.reverse();
ts_array.forEach(ts => {
if (oldest === void 0) { oldest = '1'; }
let options = {};
options['oldest'] = oldest;
options['ts'] = ts;
options['count'] = HISTORY_COUNT_PER_PAGE;
options['channel'] = channel.id;
let messages = [];
let resp;
resp = loadThreadHistory(options);
messages = resp.messages.concat(messages);
var page = 1;
while (resp.has_more && page <= MAX_HISTORY_PAGINATION) {
resp = loadThreadHistory(options, resp.messages[0].ts);
messages = resp.messages.concat(messages);
page++;
}
// 最初の投稿はスレッド元なので削除
messages.shift();
// 最新レコードを一番下にする
all_messages = all_messages.concat(messages);
console.log("channel(id:" + channel.id + ") = " + channel.name + " ts = " + ts + " => loaded replies.");
});
return all_messages;
};
// 特定チャンネルにメッセージ送信
p.postMessage = function (channel, message) {
const url = 'https://slack.com/api/chat.postMessage';
var options = {
"method": "post",
"contentType": "application/x-www-form-urlencoded",
"payload": {
"token": this.APIBotToken,
"channel": channel,
"text": message
}
};
UrlFetchApp.fetch(url, options);
}
return SlackAccessor;
})();
function PostLogSummary(slack, channel, messages, memberList) {
console.log("PostLogSummary: " + messages.length);
const COL_KEY = 0;
const COL_TEXT = 1;
var record = [];
for (let msg of messages) {
if (msg.subtype != "bot_message") {
continue;
}
var unescaped = UnescapeMessageText(msg.text, memberList);
var regexpMsgs = RegExpMessage(unescaped);
if (regexpMsgs == null) {
continue;
}
var regexpMsg = regexpMsgs[0];
var row = [];
row[COL_KEY] = regexpMsg;
row[COL_TEXT] = unescaped;
var i = record.findIndex(r => r[0] === regexpMsg);
if (i < 0) {
record.push(row);
}
else {
record[i] = row;
}
};
var msg = 'まとめ (' + record.length + '件)';
if (record.length > 0) {
record.forEach(function (v) {
msg += '\n' + v[COL_TEXT];
});
}
console.log(msg);
slack.postMessage(channel, msg);
};
コピーしておいたTokenをSetPropertiesメソッドの中の2箇所に貼り付ける。
先頭の方にあるCHANNEL_NAME、NOTIFY_CHANNEL_NAMEにNotionで接続した一時チャンネル名とまとめたUpdate情報を通知したいチャンネル名を設定する。
MOST_RECENT_HOURS に過去何時間分のログをまとめるか設定する。
GASの実行
実行する。 新しいログがなかったので過去24時間分のログをまとめた。

生のUpdateログは65件あったが、まとめた結果23件となった。
スケジューリング
GASのトリガーを作成し、まとめるログの時間を合わせることで定期的にサマリーを通知することができるようになる。