前回から続いて GAS の話。やり方を忘れる前に黙示録的に書いておく。
Slack ログの収集自体は、基本的にはこちらにあった方法を参考にした。
かいつまんでやってることを書くと、以下のような感じ。
- users.list でメンバー ID とメンバー名の辞書を作る
- channels.list で受け取ったチャンネルごとに以下を実行
- 対応する SpreadSheet ファイル – シートを取得(なければ作成)
- 最終レコードがある場合は JSON からタイムスタンプ取得して Slack ログをどこから収集するか確定させる
- channels.history で Slack のログを取得
- 取得したログをシートに書き込む
これと同じことをするだけではつまらないので、今回は Slack 上にアップロードされたデータも同時に収集する事を考えてみる。
Slack App の作成とトークン確保
Slack の旧トークン(レガシートークン) では、1トークンですべての機能が使えていた。
現在は自分で個別に Permission を設定しておく必要がある。
まずは上記の URL から Slack App を新規作成する。
「Create a Slack app」もしくは、「Manage your apps」内のボタンから新規作成が可能。
ダイアログ上で必要な情報を記入して「Create App」
「App Name」 が作成するアプリ名。
「Development Slack Workspace」 が作成先のワークスペースを指定。
作成できたら、「Basic Infomation」 → 「Permissions」 を選択。
使用する API からスコープを設定する。
今回は、以下のスコープを設定しておく。
- channels:histrory – ログを取るため必須
- channels:read – チャンネル情報を取得するのに必要
- files:read – Slack 上にアップロードされたデータも取得したいため追加
- users:read – ユーザ情報を取得するのに必要
スコープの設定が終わったら、そのページトップもしくは「Basic Information」→「Install your app to your workspace」でワークスペース上にインストールしておく。
インストール後、「OAuth & Prermissions」で発行されたアクセストークンを確認することができる。
Slack からログを取得
前準備は整ったので GAS で Slack ログを取得してみる。
// Slack へのアクセサ
var SlackAccessor = (function() {
function SlackAccessor(apiToken) {
this.APIToken = apiToken;
}
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))];
for (var k in params) {
qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
}
url += qparams.join('&');
console.log("==> GET " + url);
var response = UrlFetchApp.fetch(url);
var data = JSON.parse(response.getContentText());
if (data.error) {
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('channels.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('channels.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();
};
return SlackAccessor;
})();
function Run()
{
// API_TOKEN を指定
var slack = new SlackAccessor(API_TOKEN);
// メンバーリストの取得
var memberList = slack.requestMemberList();
// チャンネル情報の取得
var channelInfo = slack.requestChannelInfo();
// チャンネルごとにメッセージ内容を取得
channelInfo.forEach(function (ch) {
var messages = slack.requestMessages(ch, '1');
});
}
Slack へのアクセスは SlackAccessor というクラスにまとめてみた。
確保したトークンをあらかじめ指定することで、そのトークンを使用して Slack API を実行、必要な情報を取得する。
内部的には requestAPI にあるように UrlFetchApp.fetch を使用している。
SpreadSheet へ書き込み
取得したログを整理して、スプレッドシートに記述したい。
ここでは、1つのチャンネルごとに1つのシートを割り当てることを考える。
スプレッドシート編集用に以下のような関数・クラスを用意する。
function FindOrCreateFolder(folder, folderName)
{
var itr = folder.getFoldersByName(folderName);
if( itr.hasNext() ) {
return itr.next();
}
var newFolder = folder.createFolder(folderName);
newFolder.setName(folderName);
return newFolder;
}
function FindOrCreateSpreadsheet(folder, fileName)
{
var it = folder.getFilesByName(fileName);
if (it.hasNext()) {
var file = it.next();
return SpreadsheetApp.openById(file.getId());
}
else {
var ss = SpreadsheetApp.create(fileName);
folder.addFile(DriveApp.getFileById(ss.getId()));
return ss;
}
}
// スプレッドシートへの操作
var SpreadsheetController = (function() {
function SpreadsheetController(spreadsheet, folder) {
this.ss = spreadsheet;
this.folder = folder;
}
var COL_DATE = 1; // 日付・時間(タイムスタンプから読みやすい形式にしたもの)
var COL_USER = 2; // ユーザ名
var COL_TEXT = 3; // テキスト内容
var COL_URL = 4; // URL
var COL_LINK = 5; // ダウンロードファイルリンク
var COL_TIME = 6; // 差分取得用に使用するタイムスタンプ
var COL_JSON = 7; // 念の為取得した JSON をまるごと記述しておく列
var COL_MAX = COL_JSON; // COL 最大値
var COL_WIDTH_DATE = 130;
var COL_WIDTH_TEXT = 800;
var COL_WIDTH_URL = 400;
var p = SpreadsheetController.prototype;
// シートを探してなかったら新規追加
p.findOrCreateSheet = function (sheetName) {
var sheet = null;
var sheets = this.ss.getSheets();
sheets.forEach(function (s) {
var name = s.getName();
if (name == sheetName) {
sheet = s;
return;
}
});
if (sheet == null) {
sheet = this.ss.insertSheet();
sheet.setName(sheetName);
// 各 Column の幅設定
sheet.setColumnWidth(COL_DATE, COL_WIDTH_DATE);
sheet.setColumnWidth(COL_TEXT, COL_WIDTH_TEXT);
sheet.setColumnWidth(COL_URL, COL_WIDTH_URL);
}
return sheet;
};
// チャンネルからシート名取得
p.channelToSheetName = function (channel) {
return channel.name + " (" + channel.id + ")";
};
// チャンネルごとのシートを取得
p.getChannelSheet = function (channel) {
var sheetName = this.channelToSheetName(channel);
return this.findOrCreateSheet(sheetName);
};
// 最後に記録したタイムスタンプ取得
p.getLastTimestamp = function (channel) {
var sheet = this.getChannelSheet(channel);
var lastRow = sheet.getLastRow();
if(lastRow > 0) {
return sheet.getRange(lastRow, COL_TIME).getValue();
}
return '1';
};
// ダウンロードフォルダの確保
p.getDownloadFolder = function (channel) {
var sheetName = this.channelToSheetName(channel);
return FindOrCreateFolder(this.folder, sheetName);
};
// 取得したチャンネルのメッセージを保存する
p.saveChannelHistory = function (channel, messages, memberList) {
console.log("saveChannelHistory: " + this.channelToSheetName(channel));
var _this = this;
var sheet = this.getChannelSheet(channel);
var lastRow = sheet.getLastRow();
var currentRow = lastRow + 1;
// チャンネルごとにダウンロードフォルダを用意する
var downloadFolder = this.getDownloadFolder(channel);
var record = [];
// メッセージ内容ごとに整形してスプレッドシートに書き込み
messages.forEach(function (msg) {
var date = new Date(+msg.ts * 1000);
console.log("message: " + date);
var row = [];
// 日付
var date = Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
row[COL_DATE - 1] = date;
// ユーザー名
row[COL_USER - 1] = memberList[msg.user] || msg.username;
// Slack テキスト整形
row[COL_TEXT - 1] = UnescapeMessageText(msg.text, memberList);
// アップロードファイル URL とダウンロード先 Drive の Viewer リンク
var url = "";
var alternateLink = "";
if(msg.upload == true) {
url = msg.files[0].url_private_download;
// ダウンロードとダウンロード先
var file = DownloadData(url, downloadFolder, date);
var driveFile = Drive.Files.get(file.getId());
alternateLink = driveFile.alternateLink;
}
row[COL_URL - 1] = url;
row[COL_LINK - 1] = alternateLink;
row[COL_TIME - 1] = msg.ts;
// メッセージの JSON 形式
row[COL_JSON - 1] = JSON.stringify(msg);
record.push(row);
});
if (record.length > 0)
{
var range = sheet.insertRowsAfter(lastRow || 1, record.length)
.getRange(lastRow + 1, 1, record.length, COL_MAX);
range.setValues(record);
}
};
return SpreadsheetController;
})();
さらに、Run 関数を以下のように書き換える。
function Run()
{
var folder = FindOrCreateFolder(DriveApp.getFolderById(FOLDER_ID), "SlackLog");
var ss = FindOrCreateSpreadsheet(folder, "LogData");
var ssCtrl = new SpreadsheetController(ss, folder);
var slack = new SlackAccessor(API_TOKEN);
// メンバーリスト取得
var memberList = slack.requestMemberList();
// チャンネル情報取得
var channelInfo = slack.requestChannelInfo();
// チャンネルごとにメッセージ内容を取得
channelInfo.forEach(function (ch) {
var timestamp = ssCtrl.getLastTimestamp(ch);
var messages = slack.requestMessages(ch, timestamp);
// ファイル保存
ssCtrl.saveChannelHistory(ch, messages, memberList);
});
}
Slackアップロードデータの取得と保存
Slack 上にアップロードされているデータを取得する場合、Authorization: Bearer ヘッダが必要になる。
GAS から取得する場合は、以下のように options にヘッダーを指定する。
Drive への保存方法は前回行った方法と同じ。
function DownloadData(url, folder)
{
var options = {
"headers": {'Authorization': 'Bearer '+ API_TOKEN}
};
var response = UrlFetchApp.fetch(url, options);
var fileName = url.split('/').pop();
var fileBlob = response.getBlob().setName(fileName);
console.log("Download: " + url + "\n =>" + fileName);
// もし同名ファイルがあったら削除してから新規に作成
var itr = folder.getFilesByName(fileName);
if( itr.hasNext() ) {
folder.removeFile(itr.next());
}
return folder.createFile(fileBlob);
}
実行結果
実行すると指定した Test フォルダ以下に SlackLog フォルダを作成。
その中に各チャンネル別にアップロードされたデータを格納するフォルダが自動で作成される。
Slack にアップロードされた画像や動画等が保存される。
念の為、名前の一致を避けるために投稿時間を prefix としてファイル名に付加して保存してある。
ログデータが書かれたスプレッドシート LogData は以下のように各チャンネルがシートごとに分けてメッセージ内容が記録される。
あとはこのスクリプトを定期実行するように設定しておけば、定期的にログを収集してくれるようになる。
GAS は 1 度の実行時間は 6 分という制約があるので、ログの量に合わせて、6分で収まるにように収集間隔を定めるのが良いと思う。
今回作ったコードは github にもおいておいた。
コメント