BEST CLOUD さんの Slack + GAS ログ保存のコードを解読する
Tags: googlecloud, slack
Published: 2022-03-27 21:54:14


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

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

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

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

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

## setProperty で slack_api_toke や folder_id をセットする

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

```ts
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
  );
}
```

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

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

---

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

```ts
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]';
}
```

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

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

---

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

```ts
let token = API_TOKEN
```

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

```ts
let folder = FindOrCreateFolder(
  DriveApp.getFolderById(FOLDER_ID), FOLDER_NAME
);
```

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

```ts
let ss = FindOrCreateSpreadsheet(folder, SpreadSheetName);
```

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

```ts
let ssCtrl = new SpreadsheetController(ss, folder);
```

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

---

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

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

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

```ts
let slack = new SlackAccessor(API_TOKEN);
```

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

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

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

```ts
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;
  }
}
```

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

```ts
// スレッドは重い処理なので各回に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);
```

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

```ts
var p = SlackAccessor.prototype;
```

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

```ts
// 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;
};
```

```ts
  var qparams = [];
  for (var k in params) {
    qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
  }
```

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


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

```ts
https://slack.com/api/　
```

の エンドポイント　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('&');
```

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

```ts
  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;
}
```

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

```ts
// チャンネル情報取得
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;
};
```

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

```ts
// チャンネル情報取得
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;
};
```

## まとめ

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