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のトリガーを作成し、まとめるログの時間を合わせることで定期的にサマリーを通知することができるようになる。