全チャンネルに招待してくれるSlackBotを頼まれてもいないのに作ってみた

## 経緯
slack使ってますか？
最近、Unityのギルド的なslackに参加させてもらっているんですが、メンバーが200人を超えており、流行りの分報(times_\* 的なチャンネルを作って、Twitterの如く誰宛てでもなく書き込むスタイル)を推奨してる事もあり**チャンネルが全部で190チャンネルあります(2019/01/17現在)**

こうなってくると全部のチャンネルに入るのも大変です。（もちろん、絶対に全部のチャンネルに入る必要があるわけではないんですが、入らないとどういった内容のチャンネルなのかが分からないので、**「とりあえず全部のチャンネル入って、興味無さそうなチャンネルからはLeaveする」**という使い方を想定してます。）

そんなわけで自動でチャンネルにJoinするJavaScriptを頑張って改造してみたり、Chrome拡張でなんとか出来ないかな？と考えてみたりしましたが、若干作業のハードルが高い（件のギルドslackは非プログラマーの方もいるので）

**「そうだ、全チャンネルに招待(Invite)するBotを作ろう！！！」**
 僕は、（頼まれてもいないのに）bot開発に手を出したのでした・・・。

##０．ダミーのユーザー準備
 これ、やってもやらなくても良いですが、出来ればやったほうが良いです。　というのも、実は一般的に言われる「Botユーザー」では権限的にチャンネルへの招待が出来ません。そのため「LegacyToken」という、見るからにLegacyな感じのTokenを使う事になります。(実は既に非推奨）
これはユーザーに結びついているので、先にそれ専用のユーザーを作っておきます。

##１．LegacyTokenの準備
　０．で作ったユーザーでログインした状態で、 https://api.slack.com/custom-integrations/legacy-tokens 
を開き、**Legacy token generator** で、対象としたいworkspaceの行の、「CreateToken」をクリックして、Tokenを生成します（＋指定したWorkspaceにSlackAPITesterというAppが自動で追加されます）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/fc133d85-d37a-567a-4c9c-4d874a1800cf.png)

この**xoxp-**から始まるTokenは後で使うので大事に取っておきます。 以降:**LEGACY_TOKEN**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/bda47a5c-4172-6a9d-1ec2-48442a581168.png)


##２．Google Apps Script でhookを受け取れるようにしておく
自分のGoogleDriveから 「新規」→「その他」→「Google Apps Script」を選択
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/a8e28214-5e7f-91d9-f2b1-9ebfacbf3544.png)
※Google Apps Script 以降:**GAS**

GASのEditorが開きます。

多分、ファイル名が「無題のプロジェクト」になっているので、分かりやすい名前に変更しておきましょう（ここでは「SlackInviteBotとしました」)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/fa75b907-7dc2-ad6b-1910-07e93c41ec28.png)

コードはとりあえず仮で以下にしておきます。

```js
function doPost(e){

}
```

からっぽですね。
では、さっそくこの何もしないアプリを公開します。
「公開」→「ウェブアプリケーションとして導入…」を選択し
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/da100785-5048-cf88-ef3d-1e4131d2c380.png)
表示されるダイアログ上
**次のユーザーとしてアプリケーションを実行**は**自分**
**アプリケーションにアクセスできるユーザー** は **全員(匿名ユーザーを含む)**
にして、「導入」をクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/bb3ad087-411e-b585-2021-045bd9437b5b.png)

次画面で表示される**現在のウェブアプリケーションのURL**も後で使うので大事に取っておきます 以降：**HOOK_URL**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/4b7afa76-e3f7-364f-92a2-da520ce5dbc6.png)


##３．hookの準備
　次はSlack側の作業です。
　特定文字列から始まるメッセージを検知して、指定したURLへHookしてくれる便利な 通知APP があるので、まずこれをworkspaceにインストールします。

　Slack画面上、左の「App」 をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/b6722721-ffc2-d435-1491-7551442bc6a8.png)

　アプリ一覧が出てくるので、検索窓に 「Hook」まで入力。
検索上位にあるはずの「発信Webフック」の方を「インストール」してください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/3c362115-0cec-13c1-fd4a-ade32c48f55e.png)

「設定を追加」
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/79a2f050-7d12-c768-7072-2557ca7c43b5.png)

「~~インテグレーションの追加」
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/5864a24d-48c8-396f-d1f1-3f92cd156a76.png)

次画面「インテグレーションの設定」にて

- 対象チャンネル
- 引き金となる言葉
- URL

を設定します。

対象チャンネルは絞りたい場合は指定しますが、全チャンネルでも構いません。
**引き金となる言葉** は暴発されても困るので、一般的に使用しないであろう語り出しである必要があります。
僕が入っているWorkspaceでは**「:鳩」**が指定されています。今回も例としてそれで行きます（意味わかんないですけど、意外と受け入れられています。）

**URL**には 2. で取得した **HOOK_URL**を。　その他は別に設定しなくても大丈夫です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/35910d62-ea8e-34f1-9818-217d9ba0ded8.png)

そして**※最後に一番下にある「設定を保存する」を必ずクリックしてください。** 結構忘れがちです。

##４．GAS から チャンネルに発言する
また、GASに戻ってきます。
　slackのAPIを操作するのに[便利なライブラリ](https://qiita.com/soundTricker/items/43267609a870fc9c7453)が既にあるので、それを導入します。　ライブラリのキーは
「`M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO`」
になります。

ツールバーから「リソース」→「ライブラリ」を選択し
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/bf9c6cb8-73fb-59ee-a44a-42c5432fe851.png)

**ライブラリを追加**の枠にライブラリキーを入力して、「追加」→ バージョンを最新に指定 →「保存」
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/51bd24c6-66a9-a164-9970-e64a22cf3170.png)

そして、`doPost`の中身が空っぽなので、飛んできたメッセージをオウム返しするBOTに（とりあえず）します。

```js
var legacyToken = "xoxp-";//ここに、1.で取得したLEGACY_TOKENをいれてね
var postParam = {username:"鳩",icon_emoji:":bird:"}; //Botユーザー名とアイコンを指定
function doPost(e){
  var slackApp = SlackApp.create(legacyToken);
  var text = e.parameter.text;
  return slackApp.postMessage(e.parameter.channel_id, "へぇー。" + text + "なんですねー",postParam);
}
```

`var legacyToken = "";` の””の中には 1.で取得したLEGACY_TOKENを書きます。

ここで一つ注意。　飛んできたtext(e.parameter.text)を本当にそのままオウム返しすると、オウム返ししたことでまた ３．のhookが検知して、doPostが呼ばれ・・・　無限にループします（1回やった）　お気を付けを！

そして、GASの仕様なんですが、ソースコードを修正しても、**再度公開作業しない限り外部に反映されません。**
また、バージョンを「新規作成」（または「new」）にする必要があります。

再度ツールバーから「公開」→「ウェブアプリケーションとして導入」→ バージョンを「New」に→「更新」
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/a607aa60-4e2d-49f8-bb7c-8359e2c5acd1.png)

---
##承認が必要です が表示されたら
なんか、表示されるタイミングが時々で違うのですが。　とりあえず許可してあげてください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/dd75b73e-c3b8-e969-38b3-3e414c314098.png)

許可を確認→アカウントを選択
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/c21d16b0-0648-f794-1f5a-3231e6d19412.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/ea25fce4-7871-292e-42ef-96910a5f2f83.png)

---

さて、特定文字列から始まるメッセージを指定したチャンネルで（チャンネルが全てならどこでも良いので）発言しましょう。　どうでしょうか。返答は来ましたか？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/a0a81b16-9da6-bb21-ba85-d70c44345092.png)

ここで、返答が無いようだと、既に何かが間違っています。

- LEGACY_TOKENは合ってますか？　最後の文字までコピー出来てますか？
- HOOK_URLは正しく設定しましたか？
- 「設定を保存する」をちゃんと押しましたか？
- GASの公開をしましたか？　ちゃんとバージョンはNewにしましたか？


##５．ダミーのユーザーを全チャンネルにJOINさせる
　僕は1回ハマったんですが、SlackAPIのInvite(招待)はそもそもその該当のチャンネルに参加済み(Join)しているユーザーからじゃないと出来ないんですよね。
 いや、冷静に考えればそりゃそうだろという感じなのですが。　その為、**０．で作ったダミー(BOT)ユーザーは常に全チャンネルにJoinしている状態**である必要があります。

というわけで、ダミー（BOT）ユーザーを全チャンネルにJoinする処理をGASに追加します

```js
var legacyToken = "xoxp-モニョモニョ"; // ここに、1.で取得したLEGACY_TOKEN を指定
var botUserId = ""; //0. で作ったUserのIDが分かるなら指定。 ちょっと高速化されます

function allJoinTest(){
  var slackApp = SlackApp.create(legacyToken);
  var result = allJoin(slackApp);  

  Logger.log(result);  //console.logでもよいよ
}

function allJoin(slackApp){
  var channelList = slackApp.channelsList(true).channels;
  
  var cnt = 0;
  var error = 0;
  for(var key in channelList){
    var channel = channelList[key];
    if(botUserId && channel.members.indexOf(botUserId) >= 0)continue;
    var result = slackApp.channelsJoin(channel.name);
    if(result.ok)cnt++;
    else error++;
  }
  return "成功:" + cnt + ",失敗:" + error;
}
```

既に入っているチャンネルにJoinしても無駄なので2行目の

`var botUserId = ""`

で指定したユーザーが、既にJoin済みかどうかをチェックして若干の高速化を図っています。
が、面倒なら指定しなくてもよいです。というのも、このユーザーIDを調べる手段がちょっと微妙で。

案１
WebのSlackで、BotUserのユーザー名のリンクをコピー
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/b66b50cd-ebee-7d8b-2d39-e7776cbaa939.png)
このリンクの `https://****************.slack.com/team/UFG3J3C3G` 一番最後のUから始まる文字列がUserIDです。

案２
https://api.slack.com/methods/users.info/test の Tester タブで、ワークスペースを指定して、ログインIDをクリックすると、今SlackにログインしているユーザーのIDが出てくる（っぽい）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/3477edbc-5553-164f-8388-934dc3f5eabd.png)

どっちもなんだかなぁ。という感じです。　もっとスマートな方法があるなら教えてください。

閑話休題

では、allJoinTestを呼び出してみましょう。 Debugで実行します。　

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/87a43801-966e-d721-dc31-60aae072ade5.png)

Log(CTRL+ENTERで表示)を見て、成功があればOK。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/97ec9108-e288-ed38-5388-81a40c7d9b17.png)


##６．特定の発言をしたユーザーを全チャンネルにInviteする
やっと本題です。
と言っても、3.でhookの準備はできています。　4.でSlackのAPIを叩く準備もしましたし、Hookされたパラメータから情報も抜き出せてます。　5.でDummyユーザーは全チャンネルにJOIN出来るようになりました。

あとは、
- 発言したユーザーを取得
- 発言したユーザーが入っていないチャンネルに招待(Invite)
- 高速化
- BOTに小粋な発言をさせる
するだけです。（最後のは一体・・・）

今回は「:鳩よろしく」を全チャンネルInviteの発言としています。

以下、全コード

```js
var legacyToken = "xoxp-モニョモニョ"; // ここに、1.で取得したLEGACY_TOKEN を指定
var botUserId = ""; //0. で作ったUserのIDが分かるなら指定。 ちょっと高速化されます
var postParam = {username:"鳩",icon_emoji:":bird:"}; //BOTユーザーの名前とアイコン

//WebHook用 Hook発言をしたユーザー情報を取得し、全てのchannelにinviteしてあげる
function doPost(e){
  var slackApp = SlackApp.create(legacyToken);

  var text = e.parameter.text.substring(e.parameter.trigger_word.length).trim(); //受信したメッセージからe.parameter.trigger_word.length=トリガーとなった文字列　今回なら「:鳩」を削ってあげる
  
  if(text != "よろしく"){
    return slackApp.postMessage(e.parameter.channel_id, "ちょっとよくわからないです",postParam);
  }
  
  var userId = e.parameter.user_id;
  var userInfo = slackApp.usersInfo(userId);
  var userName = e.parameter.user_name;
  var displayName = userInfo.user.profile.display_name;
  var channelList = slackApp.channelsList(true).channels;
  if(!displayName)displayName = userName;
  
  var message = "どうも" + displayName + "さん。 全チャンネルに招待しますね。";
  slackApp.postMessage(e.parameter.channel_id, message, postParam); 

  //事前にTOKENユーザーをチャンネルにJOINさせる
  allJoin(slackApp);
  
  //あとはキャッシュ先行でチャンネル情報を取得しつつユーザーがJOINしてなければinviteしていく
  var cnt = 0;
  var skip = 0;
  var error = 0;
  var cache = CacheService.getScriptCache();
  for(var key in channelList){
    var channel = channelList[key];
    if(channel.members.indexOf(userId) >= 0){
      skip++;
      continue;
    }
    var channelInfo = getCachedData(cache,channel.id,function(){return slackApp.channelsInfo(channel.id);},180);
    if(channelInfo.channel.members.indexOf(userId) >= 0){
      skip++;
      continue;
    }
    var result = slackApp.channelsInvite(channel.id, userId);
    if(result.ok){
      cnt++;
    }else{
      error++;
    }
  }   
  message = "お待たせしました"+displayName+"さん！ " + skip + "件SKIP、" + error + "件ERROR、" + cnt + "チャンネルにJOIN成功しました！";

  return slackApp.postMessage(e.parameter.channel_id, message, postParam); 
}

//slackAPIの呼び出し制限が怖いので、キャッシュしてあげる
function getCachedData(cache,key,createFunc,expiredTime){
  var obj = {};
  var cacheJson = cache.get(key);
  if(cacheJson)//キャッシュされていないのであれば取得してキャッシュする
  {
    obj = JSON.parse(cacheJson);
  }else{
    obj = createFunc();
    cache.put(key,JSON.stringify(obj),expiredTime);
  }
  return obj;
}

function allJoinTest(){
  var slackApp = SlackApp.create(legacyToken);
  var result = allJoin(slackApp);  

  Logger.log(result);  //console.logでもよい
}

function allJoin(slackApp){
  var channelList = slackApp.channelsList(true).channels;
  
  var cnt = 0;
  var error = 0;
  for(var key in channelList){
    var channel = channelList[key];
    if(botUserId && channel.members.indexOf(botUserId) >= 0)continue;
    var result = slackApp.channelsJoin(channel.name);
    if(result.ok)cnt++;
    else error++;
  }
  return "成功:" + cnt + ",失敗:" + error;
}
```

先頭3行で行っている

```js
var legacyToken = "xoxp-モニョモニョ"; // ここに、1.で取得したLEGACY_TOKEN を指定
var botUserId = ""; //0. で作ったUserのIDが分かるなら指定。 ちょっと高速化されます
var postParam = {username:"鳩",icon_emoji:":bird:"}; //BOTユーザーの名前とアイコン
```

は、各環境に合わせて良しなに変更を。

そして、 **GASの変更は公開しない限り外部に反映されないので、ちゃんとバージョンを「New」にして更新**してください。

## 最終確認

それでは、（今回は）「:鳩よろしく」と打ってみましょう
[![Image from Gyazo](https://i.gyazo.com/f785b6c823d9d675e6ee22b3d87cc691.gif)](https://gyazo.com/f785b6c823d9d675e6ee22b3d87cc691)

このように、全チャンネルに心を込めて一つづつ招待されているのが分かりますね！鳩スゴイ！


## カスタマイズ
BOTの名前や、アイコン、要所要所のメッセージの言葉遣いなど、皆様の環境に合わせてカスタマイズするようお願いします。
（まともな会社でよくわからん鳩が急にチャンネルInviteし出したら頭おかしいですからね）


## 最後に
冒頭で話に上がったUnityのギルド的なslackではこの全チャンネル招待の他に

- 会話APIを絡めて鳩と小粋な会話
- 1日1回 全チャンネルの発言数・全ユーザー発言数のレポート
- 形態素解析APIを絡めて ScrapBox連携（ちょっと無理がある）

なんかをやっています(頼まれてもいないのに（2回目))。
他にも工夫や連携させるAPI次第で色々な事が出来るので、これを機にBot開発に勤しむのもどうでしょうか。

バグ報告や、何か要望等ありましたら、コメント欄にでもお気軽にどうぞ。
それでは。