【入門】Google Apps Scriptチュートリアル2/2（デモ動画付き・②コマンドを叩いてチャンネル情報を表示させる）

[前回](https://qiita.com/gkzz/items/d9f8e20a3c21061d0ece)はここまでやりました。

**1.最終的に出来上がるSlack BOTの機能**
1-1.デモ
1-2.要件をざっくりと
1-3.構成図はこんなかんじ

**2.使用するサービス、技術、フレームワークなど**
2-1.使っているもの
2-2.特筆するべき使っていないもの

**3.作成手順**
3-1.doGet()とdoPost()でHellow World!

**Appendix**
ワークスペースの作成
Appの作成
BOTの作成
BOTをチャンネルに追加
Scriptエディターを新規で作成する2つの方法
スクリプトをウェブアプリケーションとして登録し、Slash Commandsを登録

**本記事では以下の内容を取り上げます。**

**[3.作成手順](#3作成手順)**
 3-2.doGet()とdoPost()でオウム返しBOTを作る
 3-3. Slackのパブリックチャンネル名と各チャンネルの参加者数を取得
 3-4.スプレッドシートにチャンネル情報を入力
 3-5.スプレッドシートからチャンネル情報を取得し、responseを生成
 3-6.完成

**Apendix**
[userにメンションを飛ばす方法(2019/04/30現在)](#userにメンションを飛ばす方法20190430現在)
)
[スプレッドシートの表のうち特定の列を基準としてソートする](#スプレッドシートの表のうち特定の列を基準としてソートする)

#3.作成手順
##3-2.doGet()とdoPost()でオウム返しBOTを作る
**これから作るもの**

```
#/<Slash Command>  <オウム返しするテキスト（複数行でも可）>
/navigate Hello GAS!
```

とコマンドを叩いたときに、
<オウム返しするテキスト（複数行でも可。ここでは"Hello GAS!"）>
が出力される！！

![repeat.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/94a964d0-10b5-b3e3-6626-fe4f45720959.gif)

スクリプトはこちらです。

```
function doPost(e) {
    const VERIFICATION_TOKEN = "<YOUR_APP_VERIFICATION_TOKEN>";
    var paramToken = e.parameter.token;
    if (paramToken != VERIFICATION_TOKEN) {
        throw new Error('Invalid token');
    }
    var response = {
        //text: "just published"
        text: e.parameter.text 
    };
    return ContentService.createTextOutput(
        JSON.stringify(response)
    )
    .setMimeType(
        ContentService.MimeType.JSON
    );
}
```
変更箇所は、responseの中身だけです。
eに各種パラメーターが用意されているので、オウム返しさせたい場合、つまりSlash Commandsの後ろに続くテキストをresponseに代入したい場合は、

```
e.parameter.text 
```
とすればOKです。

e.parameterを実際に目で確認したい場合は、たとえばこのようにするといいかんじに確認できます。

```
function doGet(e){
    doPost(e);
}


function doPost(e) {
  const VERIFICATION_TOKEN = "xxxxxxxxxxxxxxxxxxx";
  var paramToken = e.parameter.token;
  if (paramToken != VERIFICATION_TOKEN) {
    throw new Error('Invalid token');
  }
  return ContentService.createTextOutput(
      JSON.stringify(e.parameter)
  )
}
```
以下のコマンドをslackで叩いた際に返ってきた出力結果をお見せします。

```
/hello これはテストです。
```
出力結果

```
{
    "channel_name":"sample",
    "user_id":"<USER_ID>",
    "user_name":"gakuji.tamaki",
    "trigger_id":"<TRIGER_ID>",
    "team_domain":"<TEAM_DOMAIN>",
    "team_id":"<TEAM_ID>",
    "text":"これはテストです。",
    "channel_id":"<CHANNEL_ID>",
    "command":"/hello",
    "token":"<TOKEN>",
 "response_url":"https://hooks.slack.com/commands/<xxxxxxxxx>/<yyyyyyyyyyy>/<zzzzzzzzzzzzzzzzzz>"
}
```
このようにkeyとvalueのペアで返ってきていることが確認できます。
ちなみに、e.parameterではなく、e.parameter.textとした結果をお見せします。

```
return ContentService.createTextOutput(
      //JSON.stringify(e.parameter) 
　　　JSON.stringify(e.parameter.text)
)
```

出力結果

```
"これはテストです。"
```
これでは、オウム返ししたい文字列にあたるvalueの両端にダブルクオーテーションマークがついていて邪魔ですね。
Apps Scriptのドキュメントの下記のサンプルコードを参考にこのように書き換えることができます。

```
var response = {
    text: e.parameter.text
}
return ContentService.createTextOutput(
    //JSON.stringify(e.parameter.text)
    JSON.stringify(response)
)
.setMimeType(
    ContentService.MimeType.JSON
);
```


>```
function doGet(request) {
  var events = CalendarApp.getEvents(
    new Date(Number(request.parameters.start) * 1000),
    new Date(Number(request.parameters.end) * 1000));
  var result = {
    available: events.length == 0
  };
  return ContentService.createTextOutput(JSON.stringify(result))
    .setMimeType(ContentService.MimeType.JSON);
}
```

出力結果はこのようになるようです。
"available"がkey, booleanがvalueとなっていますね。
>The service will return JSON that reports whether you have anything in your calendar in that range.


>```
{"available":true}
```

出所：[Content Service | Apps Script | Google Developers](https://developers.google.com/apps-script/guides/content)

さて、Slackのパブリックチャンネルと各チャンネルの参加者数を取得していきましょう。

##3-3. Slackのパブリックチャンネル名と各チャンネルの参加者数を取得
###3-3-1.curlでSlack APIを叩く
ターミナルで以下のコマンドを叩いてみてください。
※大量に出力されるので、パイプでlessをつなげています。

```
$ curl https://slack.com/api/conversations.list?token=xoxb-xxxxxxxxxxxxxxxxxxx | less
```

ここでは参考までに出力結果の一部を掲載します。

```
{
    "ok":true,
    "channels": 
      [
          {
              "id":"CF3JDEK6U",
              "name":"myschedule",
              "is_channel":true,
              "is_group":false,
              "is_im":false,
              "created":1546315804,
              "is_archived":false,
              "is_general":fa:false,
              "parent_conversation":null,
              "creator":"UF47K67V1",
              "is_ext_shared":false,
              "is_org_shared":false,
              "shared_team_ids":["TF5NF66DU"],
              "pending_shared":[],
              "is_pending_ext_shared":false,
              "is_member":false,
              "is_private":false,
              "is_mpim":false,
              "topic":{
                          "value":"","creator":"",
                          "last_set":0
               },
              "purpose":{
                            "value":"",
                            "creator":"",
                            "last_set":0
               },
               "previous_names":[],
               "num_members":0
         },   
[....] 
```
無事取得できることが確認できましたか？
ターミナルではcurlコマンドを使えばSlack APIを叩くことができましたが、Google Apps Scriptではcurlコマンドを使うことが出来ません。
Apps Scriptでは、curlコマンドの代わりにUrlFetch Serviceが提供されています。
下記のデモ動画のように、投稿内容をちょっとリッチにしたい際にもUrlFetch Serviceを使います。
![todays_goal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/e479ea15-771f-bd2b-a64e-4ea108c22e33.gif)


###3-3-2. UrlFetchを使う
以下のスクリプトをスクリプトエディタにコピペしてください。

```
function test_fetch() {
    const SLACK_TEAM = "<YOUR_SLACK_TEAM>";
    const SLACK_TOKEN = "xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxx";
    //var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/channels.list?token=' + SLACK_TOKEN;
    var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/conversations.list?token=' + SLACK_TOKEN;
    var listres = UrlFetchApp.fetch(listurl);
    Logger.log(listres);
    var listjson = JSON.parse(listres.getContentText());
    //Logger.log(listjson);
}
```
listurlはcurlを使った際に用いたURLを使いますが、ここではもう少し汎用性を高くするべく、ワークスペースによって異なる箇所（SLACK_TEAM）とSLACK_TOKEN（xoxbから始まるBot User OAuth Access Token）を変数としてlisturlから抜き出しています。


###3-2-1.SLACK API Method URLの取得方法
ところで、ひとつみなさんにお伝えしていない点があります。それは今回使うSLack APIのなかのconversations.listの取得方法です。

このMethod URL含めてSlack APIは下記のURLから取得することが出来ます。
https://api.slack.com/methods/conversations.list/test

https://<YOUR_WORKSPACE>.slack.com/apps から辿りたい方は以下のデモ動画をご参照ください。
![getUrl.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/0d145580-8a9e-244f-e1fd-cf18d31bcc90.gif)


https://api.slack.com/methods/channels.list

本チュートリアルでは、conversations.listを使ってチャンネル情報を取得してみましたが、Slack APIでは他にも提供されているので、本チュートリアルを読み終えた後、ドキュメントを覗いてみてください。

さて、本チュートリアルで次に取り上げることは、conversation.listで取得したチャンネル情報をスプレッドシートに書き込むことです。


##3-4.スプレッドシートにチャンネル情報を入力
![get_ch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/448f9604-00e7-fcca-6ccf-b824af24acce.gif)

```
function getChannels() {
　 //データを出力するスプレッドシートのシート名称
  const SHEET_NAME = "<YOUR_SHEET_NAME>"; 

  //スプレッドシートにシート単位でアクセス
  const SHEET = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME);

  //https://<YOUR_SLACK_TEAM>.slack.com/messages/<YOUR_CHANNEL_ID>/
  const SLACK_TEAM = "<YOUR_SLACK_TEAM>";

  const SLACK_TOKEN = "xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxx";

  //channels.listはSlackで推奨されていない
  //var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/channels.list?token=' + SLACK_TOKEN;
  var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/conversations.list?token=' + SLACK_TOKEN;
  var listres = UrlFetchApp.fetch(listurl);
  //Logger.log(listres);
  var listjson = JSON.parse(listres.getContentText());
  //Logger.log(listjson);
  for (var i = 0; i < listjson.channels.length; i++ ) {
      //(listjson.channelsのi番目のハッシュから該当の値を任意のセルに書き込んでいく
      setValues(listjson.channels, i, SHEET);
  }
  //左からN列目を昇順(true)/降順(false)とするために、
 //sort(<対象のスプレッドシートのシートオブジェクト>, N, true/false)
  sort(SHEET, 4, false);
}



function setValues(array, rowNumber, SHEET){
  var value;
  if ( !array[rowNumber] ) {
      value = "not found";
      //B列の"rowNumber+2"に"not found"
      SHEET.getRange(rowNumber+2, 1).setValue(value);
　　　//C列の"rowNumber+2"に"not found"
      SHEET.getRange(rowNumber+2, 2).setValue(value);
      //D列の"rowNumber+2"に"not found"
      SHEET.getRange(rowNumber+2, 3).setValue(value);
  }
  else {
      value = array[rowNumber];
      Logger.log(value);
      //B列の"rowNumber+2"にkeyが"name"のvalue
      SHEET.getRange(rowNumber+2, 2).setValue(value["name"]);
      //C列の"rowNumber+2"にkeyが"id"のvalue
      SHEET.getRange(rowNumber+2, 3).setValue(value["id"]);
      /D列の"rowNumber+2"にkeyが"numMembers"のvalue
      SHEET.getRange(rowNumber+2, 4).setValue(value["num_members"]);
  }
  //no. 
  //A列の"rowNumber+2"にrowNumber+1
  SHEET.getRange(rowNumber+2, 1).setValue(rowNumber+1);
}


function sort(SHEET, columnNumber, ascending){
    const OFFSET_ROW = 1;
    const OFFSET_COLUMN = 0;
    SHEET.getDataRange()
        .offset(OFFSET_ROW, OFFSET_COLUMN)
        .sort([{column:columnNumber,ascending:ascending}]);

}

```

##3-5.スプレッドシートからチャンネル情報を取得し、responseを生成

```
function generateResponse(e){
  //スプレッドシートからチャンネル情報を取得
  const SHEET_NAME = "<Your SheetName>";
  const SHEET = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME);
  var valueRange = getValueRange(SHEET);
  //Logger.log(valueRange);
  var msgs = "<@" + e.parameter.user_id + ">\n\n";
  for ( var i = 0; i < valueRange.length; i++ ) {
      if ( valueRange[i][2] ) {
        msgs += "<#" + valueRange[i][2] + ">  ";
        msgs += "参加者数：" + valueRange[i][3] + "\n\n";
      }
  }
  var response = { text: msgs };
  return response;

}

```

##3-6.完成
①コード.gs(Githubではmain.js)

```
function doGet(e){
  doPost(e);
}


function doPost(e) {
  const VERIFICATION_TOKEN = "<YOUR_APP_VERIFICATION_TOKEN>";
  var paramToken = e.parameter.token;
  if (paramToken != VERIFICATION_TOKEN) {
    throw new Error('Invalid token');
  }
  var response = generateResponse(e);
  //var response = {
  //      text: "Hellow World!" 
  //};
  return ContentService.createTextOutput(
      JSON.stringify(response)
  )
  .setMimeType(
       ContentService.MimeType.JSON
  );
}


function generateResponse(e){
  const SHEET_NAME = "<Your SheetName>";
  const SHEET = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName(SHEET_NAME);
  var valueRange = getValueRange(SHEET);
  //Logger.log(valueRange);
  var msgs = "<@" + e.parameter.user_id + ">\n\n";
  for ( var i = 0; i < valueRange.length; i++ ) {
      if ( valueRange[i][2] ) {
        msgs += "<#" + valueRange[i][2] + ">  ";
        msgs += "参加者数：" + valueRange[i][3] + "\n\n";
      }
  }
  var response = { text: msgs };
  return response;

}

```

②getChannels.gs(GithubではgetChannels.js)

```
function getChannels() {
　 //データを出力するスプレッドシートのシート名称
  const SHEET_NAME = "<YOUR_SHEET_NAME>"; 

  //スプレッドシートにシート単位でアクセス
  const SHEET = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME);

  //https://<YOUR_SLACK_TEAM>.slack.com/messages/<YOUR_CHANNEL_ID>/
  const SLACK_TEAM = "<YOUR_SLACK_TEAM>";

  const SLACK_TOKEN = "xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxx";

  //channels.listはSlackで推奨されていない
  //var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/channels.list?token=' + SLACK_TOKEN;
  var listurl = 'https://'+ SLACK_TEAM +'.slack.com/api/conversations.list?token=' + SLACK_TOKEN;
  var listres = UrlFetchApp.fetch(listurl);
  //Logger.log(listres);
  var listjson = JSON.parse(listres.getContentText());
  //Logger.log(listjson);
  for (var i = 0; i < listjson.channels.length; i++ ) {
      setValues(listjson.channels, i, SHEET);
  }
  //左からN列目を昇順(true)/降順(false)とするために、
 //sort(<対象のスプレッドシートのシートオブジェクト>, N, true/false)
  sort(SHEET, 4, false);
}


function setValues(array, rowNumber, SHEET){
  var value;
  if ( !array[rowNumber] ) {
      value = "not found";
      SHEET.getRange(rowNumber+2, 1).setValue(value);
      SHEET.getRange(rowNumber+2, 2).setValue(value);
      SHEET.getRange(rowNumber+2, 3).setValue(value);
  }
  else {
      value = array[rowNumber];
      Logger.log(value);
      //name
      SHEET.getRange(rowNumber+2, 2).setValue(value["name"]);
      //id
      SHEET.getRange(rowNumber+2, 3).setValue(value["id"]);
      //numMembers
      SHEET.getRange(rowNumber+2, 4).setValue(value["num_members"]);
  }
  //no.
  SHEET.getRange(rowNumber+2, 1).setValue(rowNumber+1);
}


function sort(SHEET, columnNumber, ascending){
    const OFFSET_ROW = 1;
    const OFFSET_COLUMN = 0;
    SHEET.getDataRange()
        .offset(OFFSET_ROW, OFFSET_COLUMN)
        .sort([{column:columnNumber,ascending:ascending}]);

}

```
#Appendix
##userにメンションを飛ばす方法(2019/04/30現在)
slackで@usernameとしているようにスクリプトでもそうすればいいかというとそういうわけではありません。
Slackのドキュメントを作成したこちらの表をご覧ください。
@channelとする機会も多いと思ったのでついでにご紹介します。
どちらもidを使います。
取得方法は、slackのドキュメントのなかで行うテスターがいいかと思います。
テスターの使い方は、[**3-2-1.SLACK API Method URLの取得方法**](###3-2-1.SLACK API Method URLの取得方法)で取り上げています。

| 仕様      　　　           | 出力結果      　　|  テスターで使うメソッド       |            
|:--------------------------|------------------|----------------------------:|
| #C024BE7LR (#channel_id)  | general          | [conversations.list](https://api.slack.com/methods/conversations.list/test)   |
| <@U024BE7LH> (<@user_id>) | @ユーザー名       | [users.list] (https://api.slack.com/methods/users.list/test)                 |



>Linking to channels and users
>For example, to refer to a channel or user within a message, a client should send the following messages:
>
>```
>Why not join <#C024BE7LR|general>?
>Hey <@U024BE7LH>, did you see my file?
>```


出所：[A lingering farewell to the username | Slack](https://api.slack.com/changelog/2017-09-the-one-about-usernames)

特定のユーザーidだけ分かればいいという場合は、@ryo-yamaoka さんの[記事](https://qiita.com/ryo-yamaoka/items/7677ee4486cf395ce9bc)で取り上げられているのでご参照ください。

チャンネルidについてはslackのチャンネルごとのページを開いた状態のURLの**/messages/**に続く文字列がそれです。

```
https://<YOUR_WORKSPACE>.slack.com/messages/<CHANNEL_ID>
```

実例としては、僕が今回取り上げているサンプルコードでは、こちらが参考になるはずです。

```
var msgs = "<@" + e.parameter.user_id + ">\n\n";
```
**e.parameter**???
どこかで聞いた言葉ですよね？？
そうです。
BOTが返すテキスト（response）を作ったときに出てきました。
ここです。

```
function generateResponse(e){
  [...]
  var valueRange = getValueRange(SHEET);
  //Logger.log(valueRange);

  var msgs = "<@" + e.parameter.user_id + ">\n\n";  //←ココです

  for ( var i = 0; i < valueRange.length; i++ ) {
  [...]
  var response = { text: msgs };
  return response;

}
```

参考：[##3-2.doGet()とdoPost()でオウム返しBOTを作る](##3-2.doGet()とdoPost()でオウム返しBOTを作る)

つまり、e.parameter.user_idとuser_idは同じ値であるので、メンションをつけてチャンネル情報を返すことができていたのです。

##スプレッドシートの表のうち特定の列を基準としてソートする
公式ドキュメントのサンプルコードを参考に実装しました。

>```
>var ss = SpreadsheetApp.getActiveSpreadsheet();
>var sheet = ss.getSheets()[0];
>var range = sheet.getRange("A1:C7");
>
>[...]
>
>// Sorts descending by column B
>range.sort({column: 2, ascending: false});
>
>```

出所：[sort(sortSpecObj) | Class Range | Apps Script | Google Developers](https://developers.google.com/apps-script/reference/spreadsheet/range#sort(Object))

僕が実装した箇所を抜粋します。

```
function sort(SHEET, columnNumber, ascending){
    const OFFSET_ROW = 1;    //頭一行目はソートする際の判定範囲とならないように除外
    const OFFSET_COLUMN = 0;
    SHEET.getDataRange()
        .offset(OFFSET_ROW, OFFSET_COLUMN)  
        .sort([{column:columnNumber,ascending:ascending}]);
}

```

**ソースコード**
[channelList.js](https://github.com/gkzz/gas-handson/blob/devel-generateResponse/channelList.js)
[getChannels.js](https://github.com/gkzz/gas-handson/blob/master/getChannels.js)

**参考**
下記の記事は、今回の実装をするうえでdoGet(), doPost(), Slash Commandsの概要を知るうえで参考にしました。
[Slash CommandsとGASでSlackのオリジナルコマンドをつくる](https://qiita.com/chikuwa111/items/7a1a349b82318a5861cc)

**前編**
[【入門】Google Apps Scriptチュートリアル1/2（デモ動画付き・①doGet()とdoPost()でHellow World!）](https://qiita.com/gkzz/items/d9f8e20a3c21061d0ece)

**番外編**
[【入門】Google Apps Scriptチュートリアル1/2（デモ動画付き・①doGet()とdoPost()でHellow World!）](https://qiita.com/gkzz/items/2003f46612a4fd3a3b7f)


P.S. Twitterもやってるのでフォローお願いします！
[@gkzvoice](https://twitter.com/gkzvoice)