BEST CLOUD さんの Slack + GAS ログ保存のコードを解読する

kaede_io

kaede

Posted on March 27, 2022

BEST CLOUD さんの Slack + GAS ログ保存のコードを解読する

https://best-cloud.jp/slack-message-log-auto-save-gas/

BEST CLOUD さんの Slack の全チャンネルのメッセージを
GAS でスプレッドシートに全て保存する記事を読んで試した

しかし、肝心のコードでは

以下のテキストを最初から最後まで全てコピーしてください。

とあり、わずかなコメントだけだったので解釈してみる

setProperty で slack_api_toke や folder_id をセットする

最初に  SerPropoerties() をしている
この関数は

function SetProperties() {
  PropertiesService.getScriptProperties().setProperty(
    'slack_api_token', 
    'xoxp-111-222-333-444'
  );
  PropertiesService.getScriptProperties().setProperty(
    'folder_id', '123ef'
  );
  PropertiesService.getScriptProperties().setProperty(
    'last_channel_no', -1
  );
}
Enter fullscreen mode Exit fullscreen mode

GAS の内部プロパティ?に埋め込む setProperty メソッドを使って
slack_api_token に xoxp-1234 などのトークン
folder_id に Google Drive のフォルダ ID
last_chanlle_no に -1

これを初手で入れている
これはわかりやすい。


getProperty で slack_api_token や folder_id でデータを取得して定数に入れる

const FOLDER_ID = PropertiesService.getScriptProperties().getProperty(
  'folder_id'
);
if (!FOLDER_ID) {
  throw 'You should set "folder_id" property from [File] > [Project properties] > [Script properties]';
}
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]';
}
Enter fullscreen mode Exit fullscreen mode

先ほど slack_api_token や folder_id にいれたトークンとフォルダID を
FOLDER_ID と API_TOKEN と言う定数に入れ直す。
そしてなかった場合のエラー処理を書く。

先ほど GAS のプロパティに入れたものを出して定数に入れるだけなので
ここもわかりやすい。


Google Drive のフォルダ、シート、シートへのコントローラを作成する

let token = API_TOKEN
Enter fullscreen mode Exit fullscreen mode

API_TOKEN を今度は小文字の変数に入れるなぜここでコピーしているかは不明。

let folder = FindOrCreateFolder(
  DriveApp.getFolderById(FOLDER_ID), FOLDER_NAME
);
Enter fullscreen mode Exit fullscreen mode

先ほどの FOLDER_ID と省略した FOLDER_NAME の文字列で
作者の自作メソッドの FindOrCreateFolder で、フォルダがない場合は作成する

let ss = FindOrCreateSpreadsheet(folder, SpreadSheetName);
Enter fullscreen mode Exit fullscreen mode

中でも同じように、 フォルダとスプレッドシート名からスプレッドシートを探して
スプレッドシートがない場合は作成する

let ssCtrl = new SpreadsheetController(ss, folder);
Enter fullscreen mode Exit fullscreen mode

そしてそのスプレッドシートのコントローラーを作成する


Slack へのアクセサを作成して

// Slack へのアクセサ
var SlackAccessor = (function () {
  function SlackAccessor(apiToken) {
  this.APIToken = apiToken;
}
Enter fullscreen mode Exit fullscreen mode

これは 156 行目にあった作者の自作クラス

let slack = new SlackAccessor(API_TOKEN);
Enter fullscreen mode Exit fullscreen mode

同じように slack のアクセサを作る

// メンバーリスト取得
const memberList = slack.requestMemberList();
// チャンネル情報取得
const channelInfo = slack.requestChannelInfo();
Enter fullscreen mode Exit fullscreen mode

そこからメンバーリストとチャンネルリストを取得する
この requestMemberList と requestChannelInfo も
後述の著者の自作メソッドなのでかなり難しい。

let first_exec_in_this_channel = true;

for (let ch of channelInfo) {
  console.log(ch.name)
  let timestamp = ssCtrl.getLastTimestamp(ch, 0);
  let messages = slack.requestMessages(ch, timestamp);
  ssCtrl.saveChannelHistory(ch, messages, memberList, token);
  if (timestamp == '1') {
  first_exec_in_this_channel = true;
  }
}
Enter fullscreen mode Exit fullscreen mode

そして channleInfo を map して console にだす
スプレッドシートの最後のタイムスタンプ?を取得し
slack からチャンネルのメッセージをチャンネルID?とそのタイムスタンプで取得し
スプレッドシートのコントローラに渡す
タイムスタンプが 1 であればチャンネルの最初で終わる?

// スレッドは重い処理なので各回に1回のみ行う
const ch_num = (
  parseInt(PropertiesService.getScriptProperties().getProperty(
    'last_channel_no')
  ) + 1
) % channelInfo.length;
console.log('ch_num');
console.log(ch_num);
const ch = channelInfo[ch_num]
console.log(ch);
Enter fullscreen mode Exit fullscreen mode

最後のチャンネル名、-1 を入れたものに +1 をしてチャンネル数で割る
チャンネルの中のそのプロパティを出力???ここは意味がわからない

var p = SlackAccessor.prototype;
Enter fullscreen mode Exit fullscreen mode

前述で作った Slack のアクセサからプロトタイプを取得して p に入れる

// API リクエスト
p.requestAPI = function (path, params) {
  if (params === void 0) { params = {}; }
  var url = "https://slack.com/api/" + path + "?";
  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;
};
Enter fullscreen mode Exit fullscreen mode
  var qparams = [];
  for (var k in params) {
    qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
  }
Enter fullscreen mode Exit fullscreen mode

クエリパラーメータの配列をエンコードして &= でくっつけるこのロジックはかなり便利そう。

そしてここで p.reqeustAPI と言う API リクエストの

https://slack.com/api/ 
Enter fullscreen mode Exit fullscreen mode

の エンドポイント URL でのラッパーを作成する。

p.requestAPI = function (path, params) {
  var url = "https://slack.com/api/" + path + "?";
  var qparams = [];
  for (var k in params) {
    qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
  }requestAPI
  url += qparams.join('&');
Enter fullscreen mode Exit fullscreen mode

引数からクエリとパスを受け取り、それらをエンドポイントの基礎 URL に加算する

  var headers = {
    'Authorization': 'Bearer ' + this.APIToken
  };
  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;
}
Enter fullscreen mode Exit fullscreen mode

そして HTTP ヘッダーの認証情報で "Bearer"+API_TOKEN をつけて
先ほどの url と 今のヘッダーをオプションとして渡し、レスポンスを受け取る
そのレスポンスを JSON にパースしてエラー処理する

// チャンネル情報取得
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;
};
Enter fullscreen mode Exit fullscreen mode

この API のラッパーをチャンネル情報の取得とメッセージの取得で使用する

// チャンネル情報取得
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;
};
Enter fullscreen mode Exit fullscreen mode

まとめ

GAS の Slack API の利用ではしっかりと API リクエストのラッパーを書いてその呼び出しにも一つ一つロジックを書いていかないと使えないことがわかった

💖 💪 🙅 🚩
kaede_io
kaede

Posted on March 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related