Slackリアクションランキング、特定のリアクションを受け取った、送った人を集計する

この記事は移行しました！[最新の内容はこちら](https://naoqoo2.github.io/blog/Slack%E3%83%AA%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%A9%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0-%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%AA%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F-%E9%80%81%E3%81%A3%E3%81%9F%E4%BA%BA%E3%82%92%E9%9B%86%E8%A8%88%E3%81%99%E3%82%8B/)をご覧ください😀


やってきました！12月！といえばアドベントカレンダーですね！
今年も最後まで楽しみましょう！

これは [株式会社マイホム Advent Calendar 2021](https://qiita.com/advent-calendar/2021/myhm)  1日目の記事です。

## はじめに

1年を振り返るにはSlackリアクションしかないでしょう！
ということでもう何番煎じかわからないSlackのリアクション集計！
ごにょごにょ実装したので投稿します。

※こちらの記事を大いに参考にさせていただきました。ありがとうございます！
https://qiita.com/ryosuk/items/1a18c663884d2748d4ca

## なぜやるか

よく使われたリアクションを知ることで、会社の特色やどんな一年だったかがわかるのでは？😃

また、弊社はMVVのVALUE

- User First
- Profesional
- Love
- Playful

という4つのカスタム絵文字があり、
それを使った人、受け取った人を知ることで
MVVを体現している人がわかっちゃうのでは！？😃😃😃😃

## できること

下記をスプレッドシートに出力します。
※プライベートチャット、スレッド内のリアクションは集計対象外。

- リアクションランキング
- MVVリアクションを受け取った回数（ユーザーごと）
- MVVリアクションを使った回数（ユーザーごと）

## 実行方法
### Slack App を作成し User OAuth Token を取得する

Slack APIを叩くために必要。

メモ：scopesを設定する必要があり、ググると昔はGUI上で設定できたっぽいが今はManifestを直接書け、みたいに言われるので直接書いた。

```
oauth_config:
  scopes:
    user:
      - channels:read
      - groups:read
      - im:read
      - mpim:read
      - channels:history
      - groups:history
      - im:history
      - mpim:history
      - users:read
```

### スプレッドシートを用意する

下記３つのシートを作成しておきます。

- ranking
- mvv_get
- mvv_send

### GASを実行する

下記コードをコピペして、チャチャっと書き換えて、main()を実行！

```js
// 参考: https://qiita.com/ryosuk/items/1a18c663884d2748d4ca
var API_TOKEN = "ここに取得したトークンをコピペ";

var target_year = '2021'; // 対象年
var mvv_reactions = ['らぶ','ぷろ','ゆーざー','ぷれい']; // MVVの絵文字名

var sheet_name_ranking = 'ranking';
var sheet_name_mvv_get = 'mvv_get';
var sheet_name_mvv_send = 'mvv_send';

// ページネーション
var MAX_HISTORY_PAGINATION = 1; // デバッグ用の定数になった
var HISTORY_COUNT_PER_PAGE = 200; // 対象年分とってくる。

var stamps = {};
var counts = [];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var timezone = ss.getSpreadsheetTimeZone();

var slack_members = {};
var mvv_get_user = {};
var mvv_send_user = {};

function main() {
  var logger = new SlackChannelHistoryLogger();
  logger.run();

  updateRankingSheet();
  updateMvvGetSheet();
  updateMvvSendSheet();

  Logger.log(mvv_get_user);
  Logger.log(mvv_send_user);
  Logger.log(slack_members);

  function updateRankingSheet() {
    var sheet = ss.getSheetByName(sheet_name_ranking);
    if (!sheet) {
      sheet = ss.insertSheet().setName(sheet_name_ranking);
    }
    sheet.clear();

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

  function updateMvvGetSheet() {
    var sheet = ss.getSheetByName(sheet_name_mvv_get);
    if (!sheet) {
      sheet = ss.insertSheet().setName(sheet_name_mvv_get);
    }
    sheet.clear();

    var data = [];
    data.push(['なまえ', ...mvv_reactions, '合計']);
    Object.entries(mvv_get_user).forEach(function([key, value]){
      var name = slack_members[key];
      if (!name) {
        return;
      }
      var tmp = Object.values(value);
      var total = tmp.reduce(function(sum, element){
        return sum + element;
      }, 0);
      data.push([name, ...tmp, total]);
    });
    sheet.getRange(1,1,data.length,6).setValues(data);

    //降順でソート
    sheet.getRange(1,1,data.length,6).sort({column: 6, ascending: false});
  }

  function updateMvvSendSheet() {
    var sheet = ss.getSheetByName(sheet_name_mvv_send);
    if (!sheet) {
      sheet = ss.insertSheet().setName(sheet_name_mvv_send);
    }
    sheet.clear();

    var data = [];
    data.push(['なまえ', '送信回数']);
    Object.entries(mvv_send_user).forEach(function([key, value]){
      data.push([slack_members[key], value]);
    });
    sheet.getRange(1,1,data.length,2).setValues(data);

    //降順でソート
    sheet.getRange(1,1,data.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 headers = {
            'Authorization': 'Bearer '+ API_TOKEN
          };
          var options = {
            'headers': headers
          };
          var resp = UrlFetchApp.fetch(url, options);
          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 usersResp = this.requestSlackAPI('users.list');
        for (const member of usersResp.members) {
          // //削除済、botユーザー、Slackbotを除く
          // if (!member.deleted && !member.is_bot && member.id !== "USLACKBOT") {
            slack_members[member.id] = member.profile.display_name;
          // }
        }

        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();
        // チャンネル絞って開発用
        // if (ch.name != 'hogehoge') {
        //   return;
        // }
        var messages = this.loadMessagesBulk(ch, {});
        var dateStringToMessages = {};


      if(messages != "err"){
        Logger.log(ch.name + ' messages:' + messages.length);
        messages.forEach(function (msg) {
          var date = new Date(+msg.ts * 1000);
          var reactions = msg.reactions ? msg.reactions : "";
          var m_year = Utilities.formatDate(date, timezone, 'yyyy');
          // チャンネル絞って開発用
          // if (ch.name == 'hogehoge') {
          //   // Logger.log(Utilities.formatDate(date, timezone, 'yyyy-MM-dd'));
          //   // Logger.log(msg);
          //   // Logger.log(reactions);
          // }
          if(reactions !== "" && m_year == target_year){
            reactions.forEach(function (reaction) {
              var name = reaction.name;
              if (stamps[name]) {
                stamps[name] = stamps[name] + reaction.count;
              } else {
                stamps[name] = reaction.count;
              }

              // MVVリアクションならさらにごにょごにょ
              if (mvv_reactions.includes(name)) {
                if (!mvv_get_user[msg.user]) {
                  mvv_get_user[msg.user] = {};
                  mvv_reactions.forEach(function (mvv_reaction) {
                    mvv_get_user[msg.user][mvv_reaction] = 0;
                  });
                }
                mvv_get_user[msg.user][name] = mvv_get_user[msg.user][name] + reaction.count;

                reaction.users.forEach(function (send_user) {
                  if (!mvv_send_user[send_user]) {
                    mvv_send_user[send_user] = 0;
                  }
                  mvv_send_user[send_user]++;
                });
              }
            });
          }
        });
      }
    };
    
    SlackChannelHistoryLogger.prototype.loadMessagesBulk = function (ch, options) {
        var _this = this;
        if (options === void 0) { options = {}; }
        var messages = [];
        options['limit'] = HISTORY_COUNT_PER_PAGE;
        options['channel'] = ch.id;
        var loadSince = function (cursor) {
          if (cursor) {
              options['cursor'] = cursor;
          }
          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) {
        // ページがあって、メッセージが対象年なら次ページを取得
        // HACK: 最初のメッセージで判定してる関係で無駄に1回多く叩いてるけど
        while (resp.has_more && target_year == Utilities.formatDate(new Date(+resp.messages[resp.messages.length-1].ts * 1000), timezone, 'yyyy')) {          
            resp = loadSince(resp.response_metadata.next_cursor);
            page++;
        }
        return messages;
    };
    return SlackChannelHistoryLogger;
})();

```

## できあがり

### ranking（利用頻度の多いリアクションランキング）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56789/07b8a1ec-5b17-5dad-b56d-d6d37871c25f.png)

### mvv_get（MVVリアクションを受け取った人）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56789/1911a497-0fb0-5052-738a-3a02586b33c6.png)

### mvv_send（MVVリアクションを使った人）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56789/dc3b1efb-124f-e6cc-1257-59e2f601c897.png)

## さいごに

集計って楽しくてワクワクしますよね♪
それではみなさん、MVVを意識して仕事していきましょー！