[GAS] Slack ログをアップロードデータを含めて GoogleDrive に自動保存する

前回から続いて GAS の話。やり方を忘れる前に黙示録的に書いておく。


Slack ログの収集自体は、基本的にはこちらにあった方法を参考にした。

かいつまんでやってることを書くと、以下のような感じ。

  1. users.list でメンバー ID とメンバー名の辞書を作る
  2. channels.list で受け取ったチャンネルごとに以下を実行
    • 対応する SpreadSheet ファイル – シートを取得(なければ作成)
    • 最終レコードがある場合は JSON からタイムスタンプ取得して Slack ログをどこから収集するか確定させる
    • channels.history で Slack のログを取得
    • 取得したログをシートに書き込む

これと同じことをするだけではつまらないので、今回は Slack 上にアップロードされたデータも同時に収集する事を考えてみる。

Slack App の作成とトークン確保

Slack の旧トークン(レガシートークン) では、1トークンですべての機能が使えていた。
現在は自分で個別に Permission を設定しておく必要がある。

Starting with Slack apps
Learn how to build bot users, send notifications, and interact with workspaces using our APIs.

まずは上記の 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 &amp;&amp; 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 ヘッダが必要になる。

Unsupported Browser | Slack

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 にもおいておいた。

negimochi/SlackLogGAS
Contribute to negimochi/SlackLogGAS development by creating an account on GitHub.