Google Apps Scriptでシャッフルランチのグループ分けをしてくれるSlack Botをつくる

いま働いてる職場では、シャッフルランチといって、定期的にメンバーをシャッフルしてランチをする機会があるのですが、人数が多いとグループ分けになかなか骨が折れるので自動化してみました。

## こんなことができます

「○人ずつシャッフルして」とメンションを飛ばすと、そのチャネルにいるメンバー一覧を取得して、ランダムにグルーピングしてくれます。
<img width="690" alt="スクリーンショット_2019-06-20_17_36_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/611/d2156c26-0f62-20c3-99df-56f312c41165.png">
（モザイクだらけ・・ :joy: ）

以下、思い出しながら書いてるので情報が断片的です。 :bow: 

## 実装手順

### slack botをつくる
botの作り方はドキュメントに書いてるので見てください。 :pray: 
まずappを作成してbotを作成、という流れです。
[Enabling interactions with bots \| Slack](https://api.slack.com/bot-users)

ここで作成したbot userのトークンを利用します。（`xoxb-`からはじまるやつ）
![スクリーンショット_2019-06-20_17_34_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/611/6e6ae424-c18b-2201-3f56-41e71b64e202.png)

今回は[conversations\.members method](https://api.slack.com/methods/conversations.members)を使ってメンバー一覧を取得するので、appのOAuth & Permissionsで`channels:read`, `groups:read`を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/611/2e98edf0-6897-1d70-bf62-980de52ad3f4.png)


### Google Apps ScriptでAPIをつくる
[Apps Script](https://script.google.com)から「新規スクリプト」をポチってください。

で、以下のコードをコピペ。

```js
// 先ほど作成したbotのトークンをコピペ
var botToken = 'xoxb-THIS_IS_BOT_TOKEN';

function members_endpoint(channel, cursor) {
  return 'https://slack.com/api/conversations.members?token='+botToken+'&limit=1000&channel='+channel+'&cursor='+cursor;
}

function user_endpoint(user) {
  return 'https://slack.com/api/users.info?token='+botToken+'&user='+user;
}

function doPost(e) {
  var postData = JSON.parse(e.postData.getDataAsString());
  var res = {};
  
  if(postData.type == 'url_verification') {
    res = {'challenge':postData.challenge}
    return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
  }
  
  var channel = postData.event.channel;
  var user = postData.event.user;
  var text = postData.event.text;
  var ts = postData.event.ts;
  
  // なぜかslack apiで複数回叩かれてしまうバグ（仕様？）があるのでキャッシュを利用して複数回応答を阻止
  var cache = CacheService.getScriptCache();
  var cacheKey = channel + ':' + ts;
  var cached = cache.get(cacheKey);
  if (cached != null) {
    console.log('対応済みなのでなにもしないぞ！');
    return;
  }
  cache.put(cacheKey, true, 1800); // 30分キャッシュする

  // 「シャッフル」という単語意外には適当な言葉を返す
  if (text.indexOf('シャッフル') == -1 ) {
    replyToSlack(channel, user, getRandomText());
    return;
  }
  
  var members = shuffle(getMembers(channel));
  // ユーザの名前を取得
  var memberNames = getMemberNames(members);
  var chunk = getChunk(text);
  var texts = textsOfGroup(memberNames, chunk);
  var messages = [];
  messages.push(chunk+'人ずつシャッフル！！');
  messages.push('```');
  messages = messages.concat(texts);
  messages.push('```');
  replyToSlack(channel, user, messages.join("\n"));
  
  // なにも返さなくても200 statusが返却されるので放置
  res = {'ok': true};
  return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}

function getChunk(text) {
  var result = text.match(/([0-9０-９]+)人/);
  // chunkの指定がない場合は3人ずつにする
  if (!result) {
    return 3;
  }
  // 人数の入力が全角だった場合半角にする
  var number = result[1].replace(/[Ａ-Ｚａ-ｚ０-９]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 65248);
  });
  return parseInt(number);
}

function getMemberNames(members) {
    // ユーザの名前を取得
  var memberNames = []
  for (var i = 0; i < members.length; i++ ) {
    var member = members[i];
    var response = UrlFetchApp.fetch(user_endpoint(member));
    var json = JSON.parse(response.getContentText());
    // bot, 削除ユーザは無視
    if (json.user.is_bot || json.user.deleted　) {
      continue;
    }
    memberNames.push(json.user.name);
  }
  return memberNames;
}

function shuffle(a) {
    var j, x, i;
    for (i = a.length - 1; i > 0; i--) {
        j = Math.floor(Math.random() * (i + 1));
        x = a[i];
        a[i] = a[j];
        a[j] = x;
    }
    return a;
}

function getRandomText(){
  var texts = [
    'ごめん',
    'せやかて',
    'まじか',
    'ありがとう',
    'たしかに',
    'ありっちゃあり',
    'そうなんや',
    'それな',
    'そっかぁ',
    'うーん',
    'どうしよかな',
    'ほんまに？',
    'うんうん',
    'へぇ〜',
    '一理あるな'
 ];
  
  return texts[Math.floor(Math.random() * texts.length)];
}

function textsOfGroup(array, chunk) {
  var textsOfGroup = [];
  var i,j;
  for (i = 0, j = array.length; i < j; i += chunk) {
    var group = array.slice(i, i+chunk);
    var alphabet = columnToLetter((textsOfGroup.length+1));
    var text = alphabet + '：' + group.join('、');
    if (group.length == 1) {
      text = text + ' ← 一人なのでどこかのグループにマージしてね';
    }
    textsOfGroup.push(text);
  }
  return textsOfGroup;
}

function getMembers(channel) {
  var cursor = '';
  var members = [];
  do {
    var response = UrlFetchApp.fetch(members_endpoint(channel, cursor));
    var json = JSON.parse(response.getContentText());
    for(var i = 0; i < json.members.length; i++ ) {
      var member = json.members[i];
      members.push(member);
    }
    cursor = json.response_metadata.next_cursor;
  } while (cursor != '');
  
  return members;
}

function replyToSlack(channel, user, message){
  var url = 'https://slack.com/api/chat.postMessage'
  
  var data = {
    'channel' : channel,
    'text' : '<@'+user+'> '+message,
    'as_user' : true
  };
  
  var options = {
    'method' : 'post',
    'contentType' : 'application/json; charset=UTF-8',
    'headers' : {'Authorization': 'Bearer '+botToken},
    'payload' : JSON.stringify(data)
  };
  
  var response = UrlFetchApp.fetch(url, options)
  console.log(response.getContentText());
}

// 1 -> A のようにアルファベットにする
function columnToLetter(column) {
  var temp, letter = '';
  while (column > 0)
  {
    temp = (column - 1) % 26;
    letter = String.fromCharCode(temp + 65) + letter;
    column = (column - temp - 1) / 26;
  }
  return letter;
}
```

細かい説明は省きますが、だいたい以下のようなことをしてます。

- リクエストが飛んできたチャンネルの情報を取得してメンバー一覧を取得
- そのメンバーを適当にシャッフルして整形して返答する
- 「シャッフル」以外の単語で話しかけられたら適当に返答する

### webアプリケーションとして公開
さきほどのスクリプトをWebアプリケーションとして公開します。
公開 > ウェブアプリケーションとして導入 をクリックして公開します。
ここで発行されたURLをコピーしておきます。

### botがメンションに反応できるようにする
botもAPIもできたので、あとは連携させるだけですね。

botがメンションに反応できるように、Event Subscriptionの設定をします。
Request URLに先程発行されたAPIのURLを入力し、 `app_mention`をsubscribeします。
![スクリーンショット_2019-06-20_17_34_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/611/1833e8e4-8a86-e3f2-f843-6613f2734d19.png)

![スクリーンショット 2019-06-20 17.34.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/611/5ec843da-8b95-288c-34a1-52025ddbd1ea.png)

以上です。

あとは、botをチャネルに招待して、メンションを飛ばせばOK。 :ok_hand: 

botの権限が足りない等、漏れがあるかもしれません。良ければコメントでご指摘ください。

ではでは :wave: