GASでslackのスタンプ数を集計して順位付けし、結果をslack内のチャンネルに通知する

# 目的
どのslackのスタンプをどれだけ使っているか可視化するためにGASでスクリプト作成しました。

# 参考にした記事
https://qiita.com/_y_s_k_w/items/b144f9873a5764c71188

# 概要
slackのworkspaceのAPIトークンを取得してGASに埋め込み、
APIメソッドを使ってslackから会話の情報からスタンプの利用分(月単位)を抽出して集計します。

- [conversations.list](https://api.slack.com/methods/conversations.list)
所属するworkspacesの全てのチャンネルの一覧を取得するメソッドです。

- [conversations.history](https://api.slack.com/methods/conversations.history)
会話の履歴を取得するメソッドです。

抽出した結果をスプレッドシートに書き出し、書き出した内容をBotとしてslackの指定したチャンネルにpostします。

# 構築手順

### slackAPIの取得
slack apiの[アプリ作成画面](https://api.slack.com/apps)から、「Create New App」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/2b243671-ac70-a0b2-cda7-f0d91357344a.png)

適当な名前をつけてBasic infomation画面にて**OAuth & Permissions**を選択し、`OAuth Access Token`と`Bot User OAuth Access Token`を生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/e7691677-8b03-d68b-83bb-1de384f295be.png)

Scopes設定項目から`User Token Scopes`としてAPIメソッドに対する許可を追加します。
conversations.listとconversations.historyに必要な以下のスコープを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/faf94353-4ecd-136b-0ace-bc19846a79a9.png)

- conversations.list
  - channels:read
  - groups:read
  - im:read
  - mpim:read 

- conversations.history
  - channels:history
  - groups:history
  - im:history
  - mpim:history

また、Bot Token Scopesとして`chat:write`をスコープに追加します。 

### GASの作成
以下の通り記述します。

```js:main.gs
function main() {
     StoreLogsDelta();
     Ranking();
     slack();
}
```

```js:calculate.gs
var API_TOKEN = "OAuth Access Tokenを記載";
var BOT_TOKEN = "Bot User OAuth Access Tokenを記載";
// ページネーション
var MAX_HISTORY_PAGINATION = 10;
var HISTORY_COUNT_PER_PAGE = 1000;
var stamps = {};
var counts = [];
var names = [];
var sheet = SpreadsheetApp.getActiveSheet();
// 時間の判定
var timezone = sheet.getParent().getSpreadsheetTimeZone();
var now = new Date;

// ボタン表示
var meetingDate = Browser.inputBox("ランキングを出力したい期間(yyyy-MM)を入力してください。", Browser.Buttons.OK_CANCEL);

if(meetingDate == "cancel"){
  Browser.msgBox("スクリプトを終了します。");
} else {
  var targetmonth = Utilities.formatDate(new Date(meetingDate), timezone, 'yyyy-MM');
//入力制限が適当なので随時修正ください。
  Browser.msgBox( targetmonth + "の期間のランキングを出力します。");
}


function StoreLogsDelta() {
  var logger = new SlackChannelHistoryLogger();
  logger.run();
  //シートを削除
  sheet.clear();
  sheet.getRange(1,1).setValue(targetmonth);

  var names = Object.keys(stamps);
  var ary = [];
  for (var i=0; i<names.length; i++) {
    ary.push([names[i]]);
  }
  sheet.getRange(2,1,ary.length,1).setValues(ary);
  for (var i=0; i<names.length; i++) {
    counts.push([stamps[names[i]]]); 
  }
  sheet.getRange(2,2,counts.length,1).setValues(counts);
  //降順でソート
  sheet.getRange(2,2,counts.length,2).sort({column: 2, ascending: false});
};

var SlackChannelHistoryLogger = (function () {
    function SlackChannelHistoryLogger() {
        this.memberNames = {};
    }
    SlackChannelHistoryLogger.prototype.requestSlackAPI = 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('&');
        try{
          var resp = UrlFetchApp.fetch(url, {
              headers: { "Authorization": `Bearer ${API_TOKEN}` }, 
          });
          var data = JSON.parse(resp.getContentText());
          if (data.error) {
            throw "GET " + path + ": " + data.error;
          }
          return data;
        }catch(e){
          return "err";
          }
    };
    SlackChannelHistoryLogger.prototype.run = function () {
        var _this = this;
        var channelsResp = this.requestSlackAPI('conversations.list');
            for (var _i = 0, _a = channelsResp.channels; _i < _a.length; _i++) {
              var ch = _a[_i];
              this.importChannelHistoryDelta(ch);
            }
      
    };  
    SlackChannelHistoryLogger.prototype.importChannelHistoryDelta = function (ch) {
        var _this = this;
        var now = new Date();
        var oldest = '1'; // oldest=0 does not work
        var messages = this.loadMessagesBulk(ch, { oldest: oldest });
        var dateStringToMessages = {};


      if(messages != "err"){
        messages.forEach(function (msg) {
          var date = new Date(+msg.ts * 1000);
          var rec = msg.reactions ? msg.reactions : "";
          var m_date = Utilities.formatDate(date, timezone, 'yyyy-MM');
              if(rec !== "" && m_date == targetmonth){
                var name = rec[0].name;
          Logger.log(m_date);
                var tmp = 0;
                if (stamps[name]) {
                  tmp = stamps[name];
                  stamps[name] = tmp + rec[0].count;
                } else {
                  stamps[name] = rec[0].count;
                }      
              }
        });
      }

    };
    SlackChannelHistoryLogger.prototype.loadMessagesBulk = function (ch, options) {
        var _this = this;
        if (options === void 0) { options = {}; }
        var messages = [];
        options['count'] = HISTORY_COUNT_PER_PAGE;
        options['channel'] = ch.id;
        var loadSince = function (oldest) {
            if (oldest) {
                options['oldest'] = oldest;
            }
            var resp = _this.requestSlackAPI('conversations.history', options);
          if(resp != "err"){
            messages = resp.messages.concat(messages);
          }
            return resp;
        };
        var resp = loadSince();
        var page = 1;
        while (resp.has_more && page <= MAX_HISTORY_PAGINATION) {
            resp = loadSince(resp.messages[0].ts);
            page++;
        }
        return messages.reverse();
    };
    return SlackChannelHistoryLogger;
})();
```

```js:notify.gs
function Ranking() {
  var range = sheet.getRange(1, 1, sheet.getLastRow(), 2);
  var s_cnt = range.getValues();

  var tmp = 0;
  var tmp_n = "";

  for (var j=1; j<s_cnt.length; j++) {
    for (var k=j+1; k<s_cnt.length; k++) {
      if(s_cnt[j][1] < s_cnt[k][1]){
         tmp =  s_cnt[j][1];
        s_cnt[j][1] = s_cnt[k][1];
        s_cnt[k][1] = tmp;
         tmp_n =  s_cnt[j][0];
        s_cnt[j][0] = s_cnt[k][0];
        s_cnt[k][0] = tmp_n;
      }
    }
  }
  sheet.getRange(1,1,s_cnt.length,2).setValues(s_cnt);
  var snt = "年月:" + targetmonth
  for (var j=1; j<s_cnt.length; j++) {
    var snt = snt + "\n"+j+"位: :"+s_cnt[j][0] + ": " +s_cnt[j][1] + "回 \n"
  }
  Logger.log(snt);
  slack(snt,s_cnt[1][0]);
};

function slack(message,name) {
    var url        = 'https://slack.com/api/chat.postMessage';
    var token      = BOT_TOKEN;
    var channel    = '結果を出力するチャンネル名';
    var text       = message;
    var username   = 'GetStamp';
    var icon_emoji = ':'+name+':';

    var parse      = 'full';
    var method     = 'post';

    var payload = {
        'token'      : token,
        'channel'    : channel,
        'text'       : text,
        'username'   : username,
        'parse'      : parse,
        'icon_emoji' : icon_emoji,
        'link_names' : true
    };

    var params = {
        'method' : method,
        'payload' : payload
    };
    var response = UrlFetchApp.fetch(url, params);
};
```

# 実行/結果

## GAS実行
main.gsを実行すると、集計スプレッドシート上に出力期間の入力を求めるメッセージボックスが表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/6f554213-fe86-fc86-a4e0-ad7d3fa09d1f.png" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/600219f6-9594-e5b1-f68f-09daf59e2e96.png"  width=50%>


## スプレッドシート
このような形で記述されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/0ee61b53-f4fd-57d1-b327-3fb6300bc19c.png)


## slackチャンネルに通知
このような形で通知されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/58fec31f-332e-56ec-81db-d7f492a5221d.png" width=20%>