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

WebAPI

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


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

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

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

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

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

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

Introduction to Slack apps
Intro to Slack apps.

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

API file type
Discover the core objects you'll find throughout the Slack platform

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

SlackLogGAS/SlackLog.gs at master · negimochi/SlackLogGAS
Contribute to negimochi/SlackLogGAS development by creating an account on GitHub.

コメント