Slack Appに挑戦(5) - WebAPI

「SlackBotを真面目に学ぶ」の第5回
今日はWebAPIで遊んでみます

#はじめに
これまで、Incoming Webhook, Slash Command, Bot User, Event APIを勉強してきました。今回はWebAPIを使ってシステム側からSlackに対してアクションを起こすことを中心に学んでいきます。

##環境
+ Heroku
  + PHP 7.3
  + Laravel 6.0.3
  + guzzle 6.3
+ Windows
  + PHP 7.2.11
  + Laravel 6.0.3
  + guzzle 6.3

前回まで同じです。

#SlackのAPIを知る

##APIの種類
SlackのAPIには6種類のAPIがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/472a9a4f-c9ed-90dc-01d8-b47976c03a08.png)

:two:EventAPIは済んでいるので、今日は:one:WebAPIと:three:ConversationAPIあたりです。ConversationAPIはWebAPIの中に組み込まれていたChannel、Group, im, mpimを1つにまとめたもので、今までより会話を操作しやすくしたものです。1年前くらいにAPI使ったときにはなかったので、最近できたみたいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/cdf3ae6c-cf1a-d902-caaa-b957774a7a90.png)

:five:SCIM（System for Cross-Domains Identity Management）APIは、主にアカウントの管理をするためのAPIです。SCIMという規格（仕様）[^scim]に則ったAPIのようです。PlusプランかEnterprise Gridでしか使えないのでサンプル対象外にします。
:six:Enterprise Gridプランで使えるAPIで、セキュリティ系のログがAPIで確認できます。

[^scim]: http://www.simplecloud.info/


#WebAPI
Slackのほとんどの操作を行うことができるAPIです。やれることはたくさんあります。ユーザグループの操作、チャンネルの操作などほとんどの操作をAPI経由で行うことができます。過去のメッセージを読むのができるのはWebAPIだけです。

https://api.slack.com/web
https://api.slack.com/methods
https://api.slack.com/docs/conversations-api

##WebAPIの使い方
SlackのWebAPIは完全なRESTではなく、HTTP MethodはGETかPOSTだけです。APIごとに決まっているのでドキュメントで確認しましょう。POSTの場合でも、name=valueのformパラメータ形式であったり、JSONを受け取ってもらえたり、各メソッドごとに違いがあります。

##マニュアルの見方
例としてusers.listのページを見てみます。
https://api.slack.com/methods/users.list

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/02f6942d-ff02-7f5e-81c6-acd7191170d9.png)

だいたい見てのとおりです。

**Method URL:**
APIのエンドポイントです。URIはAPIのメソッドごとに決まっています。

**Preferred HTTP method:**
GETかPOSTです。Preferredなので緩い希望です。マニュアルにPOSTと書いてあってもでもGETでアクセスできますし、逆も大丈夫です。

**Accepted content types:**
`application/x-www-form-urlencoded`, `application/json`のどちらかです。
[chat.postMessage](https://api.slack.com/methods/chat.postMessage)のように両方受け取ってくれるメソッドもあります。

**Rate Limiting:**
アクセス回数の制限です。これはAPIのごとに決められています。users.listは`Tier2`の制限です。Tier2は1分間に20回のアクセスが可能なAPI。上限を過ぎるとレスポンスコード429が返ってくるので上手にハンドリングする必要があります。
https://api.slack.com/docs/rate-limits


```
{
    "ok": false,
    "error": "ratelimited"
}
```

**Works with:**
このAPIを使うのに必要なScopeが記載されています。bot Tokenの場合は、botスコープが必要でuser Tokenの場合には、users:readスコープが必要です。Appが必要としているスコープを持っていないトークンでAPIを実行しようとすると、以下のようなエラーが返ってきます。users.readが必要だけど、受け取ったトークンはusers.readを持っていないよ。

```
{
    "ok": false,
    "error": "missing_scope",
    "needed": "users:read",
    "provided": "identify,bot,commands,incoming-webhook,channels:history,groups:history,channels:read,groups:read,reactions:read,chat:write:user,identity.basic"
}
```

追加で許可を与えるには、`OAuth & Permission`メニューから設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/66735fbc-73ad-683b-5b5a-7c323a2670ad.png)


##APIの認証
WebAPIは、アプリケーション側からSlackに話掛ける仕組みなので、誰でもアクセスできてしまいます。正当なアクセスかどうかを判断するために、Tokenを使います。Tokenが一致していれば、本物という判断ですね。Tokenは容易に知られないように管理する必要があります。

Tokenには、複数の種類がありますが、主に使うものはユーザトークンとボットトークンの2種類です。
https://api.slack.com/docs/token-types

###User Token
>User tokens represent workspace members. They are issued for the user who installed the app and for users who authenticate the app. When your app asks for OAuth scopes, they are applied to user tokens. You can use these tokens to take actions on behalf of users.

- User token strings begin with xoxp-
- User tokens gain the "old world" resource-based OAuth scopes requested in the installation process (example: asking for channels:history grants access a user token access to channels.history for any public channel)
- User tokens represent the same access a user has to a workspace -- the channels, conversations, users, reactions, etc. they can see
- Write actions with user tokens are performed as if by the user themselves

###Bot Token
>Bot user tokens are special and require a bot user and the bot scope.
Bot user tokens represent a bot associated with the app installed in a workspace. Bot user tokens are provided only if the app includes a bot user and explicitly asks for the bot OAuth scope during installation. Bots are generally associated with conversational apps but they can do more than that (and bot-less apps can be conversational, too).
Bot user token strings begin with xoxb-
Bot user tokens have a broad set of permissions that cannot be modified
Bot user tokens can't have resource-based OAuth scopes added to them, any scopes other than bot requested during the OAuth installation flow have no effect on the bot user token
"Bots" typically switch between using both a bot user token and any number of user tokens to complete their operations
Revoking a bot user token with auth.revoke does not uninstall the bot user. A new token may be obtained via OAuth or, for internal integrations, your app management console.


##具体的にTokenを確認
TokenはAppの`OAuth & Permission`で確認することができます。

AppのTokenページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a30279c0-ab2e-a05f-42af-84d120731cc4.png)

AppにBotUserを追加したあとだと、OAuthメニューにTokenが2つ表示されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/697b3284-60aa-ab38-4866-13ed257ce7a7.png)


1つ目のOAuth Tokenは誰のトークンなのか？これはApp開発者のTokenであり、たいていは自分のトークンです。これを使って操作する場合App作者のアカウントからのアクションとみなされます。Collaborator（共同開発者）を設定すると、CollaboratorのアカウントでもAppの設定ができるようになり、別ノユーザートークンが発行されます。Bot Tokenは同じものが見えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/1aef1895-98c2-a9c1-2027-741834aaea1c.png)


#APIを呼び出すプログラムを作る
実際にAPIを呼び出すプログラムを作ってみます。ウェブアプリケーションである必要がないので、LaravelのCommandの仕組みで作ります。簡単に言うとバッチですね。参照系のAPIと更新系のAPIで権限Scopeと呼び出し方法が異なるので、２つやってみます。

#参照系のAPI
参照系のAPIは呼び出しは簡単ですが、Paginationの仕組みを理解しておく必要があります。SlackのAPIは、UserIDやChannelIDなどのIDを求めてくるケースが多いのですが、IDを知る方法がなく参照系のAPIでIDを調べるケースが多くなりがちです。応用がききそうなサンプルを1つ作っておきます。

##Pagination
大量の検索結果を分割する仕組みです。
`limit`を指定したクエリーのレスポンスに`next_cursor`があるときは、まだ結果が取り切れていないという意味です。`next_cursor`の値を次回のクエリーの`cursor`パラメータに入れて送ると、結果の続きが取れるという仕組みです。続きがないときは`next_cursor`の値が空になるので、そこで判断します。
limitの上限は1000で100-200くらいにせよとマニュアルにありました。

ページネーションの仕様はこちらにあります。
https://api.slack.com/docs/pagination

##users.list

###API仕様
URI https://api.slack.com/methods/users.list

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/5d7ae587-c952-5969-f19c-a2eb092dd828.png)

必須項目は、`token`だけです。GETで呼び出しできるので、簡単に使えます。

###コード
いつも通り、artisanコマンドでCommandのひな型を生成し、実装をしていきます。
paginationを確認するためにlimitはあえて3にしています。

```
php artisan make:command SlackUserList
```

```php:app/Console/Commands/SlackUserList.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SlackUserList extends Command
{
    protected $signature = 'slack:user-list';

    protected $description = 'call users.list API';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        //httpの通信ログを見たいときはdebug=trueにする
        $option = ['debug'=>false, 'verify'=>false, 'http_errors'=>false];
        $guzzile = new \GuzzleHttp\Client($option);

        $api = 'https://slack.com/api/users.list';
        $token = 'xoxp-472869539026-4xxxx';
        //$token = 'xoxb-472869539026-7xxxx';
        $limit = 3;
        $apiListName = 'members';
        $nextCursor='';

        $apiResult = collect([]);
        do{
            $param['cursor']=$nextCursor;
            $param['token']=$token;
            $param['limit']=$limit;
            
            $this->info('API call with ' . $nextCursor );
            $response = $guzzile->get($api, ['query' => $param]);

            // formをPOSTしたいときはこっち
            // $response = $guzzile->post($api, ['form_params' => $param]);

            $body = $response->getBody()->getContents();
            $json = json_decode($body, true);
            if( $json['ok'] == false){
                $this->info('slack returns error');
                $this->info($body);
                return 1;
            }
            
            $list = collect($json[$apiListName]);
            $this->info($list->count() . ' 件取得');

            $apiResult = $apiResult->concat($list);

            $nextCursor = $json['response_metadata']['next_cursor']??'';

            $this->info('next cursor is ' . $nextCursor);
        }while(!blank($nextCursor));
        
        $this->info('End of calling API.');

        //カウント
        $this->info('総数=' . $apiResult->count());

        //name=>idに変形
        $apiResult->pluck('id', 'name')->each(function($item, $key){
            $this->info($key . ' => ' . $item);
        });
        
        //コレクションから名前で取り出す
        $bill = $apiResult->where('name', 'bill');

        //id=>{member}に変形
        // $map = $apiResult->mapWithKeys(function ($item) {
        //     return [$item['id'] => $item];
        // });
        // var_dump($map);        

    }
}

```

###実行
実行すると、こんな感じでログが出ます。（適度に改行を入れています）

```:コマンドライン
php artisan slack:user-list

API call with
3 件取得
next cursor is dXNlcjpVTkZQSDRRMlc=
API call with dXNlcjpVTkZQSDRRMlc=
3 件取得
next cursor is dXNlcjpVTlVHWE1YUVE=
API call with dXNlcjpVTlVHWE1YUVE=
2 件取得
next cursor is
End of calling API.

総数=8

slackbot => USLACKBOT
kanaxx-user => UDX2143CM
slalack-botuser => UN9R2L9PV
bill => UNFPH4Q2W
clara => UNFPH4URY
lobot => UNL5DFMQU
alice => UNUGXMXQQ
polly => UNUJF8WRK

```

今回はユーザの取得でデータ量が大したことないので、全ユーザ情報を`$apiResult`のコレクションの中に保持しています。データ量が多いときには何かしら考えたほうがいいコードです。
全データを取得した後は、LaravelのCollectionに頼り切ったコードです。`$apiResult->where('name', 'bill')`でbillのデータが取れるとか、簡単過ぎて泣けます。

Laravelの場合、全データを取得したあとにCacheに入れるようにしておくと、WebでもBatchでもAPIを叩かずに参照できるようになるので、やってみてください。

#更新系のAPI
更新系はform postする形式とjsonをpostする形と2種類ありますが、仕様頻度が高そうなjsonをPOSTする形式のコードサンプルです。
対象のAPIは使用頻度が高そうなchat.postMessageにします。Slackにメッセージを送るやつです。

##chat.postMessage
チャンネルを指定してメッセージを送ります。ちょっと前まではAttachment形式で送信するメッセージを組み立てていましたが、新しくBlockKit形式というメッセージ形式がサポートされたので、Blockのほうを使っています。

###APIの仕様
https://api.slack.com/methods/chat.postMessage
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/eee34528-ca05-94ee-9809-8aeadbebf0f8.png)

必須項目は、`token`, `channel`,`text`の3つです。ただし、`block`を使うときは`text`は指定しなくても大丈夫です。`channel`はチャンネル名ではなくてIDの指定です。

###コード

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SlackChatPost extends Command
{
    protected $signature = 'slack:chat-post';

    protected $description = 'Command description';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $api = 'https://slack.com/api/chat.postMessage';
        $userToken = 'xoxp-472869539026-4xxx';
        $botToken = 'xoxb-472869539026-7xxx';
        $token  = $userToken;

        //httpの通信ログを見たいときはdebug=trueにする
        $option = ['debug'=>true, 'verify'=>false, 'http_errors'=>false];

        //JSONをPOSTするときは、トークンはHeaderに入れる
        $option['headers'] = ['Authorization'=>"Bearer $token"];
        
        $guzzle = new \GuzzleHttp\Client($option);

        //POSTするJSON用のPHP配列
        $payload = [];
        //通知画面に出るメッセージ
        $payload['text']='あああ <!here>';

        //チャンネルはIDで指定（チャンネル名ではないので注意）
        $payload['channel']='CDWRKG3SN';//general
        
        $payload['as_user']=true;
        
        //Block Kit形式のメッセージを作成
        $message = 'ただいま' . date('Y-m-d H:i:s');
        $section = ['type'=>'section', 'text'=>['type'=>'mrkdwn', 'text'=>$message]];
        $divider = ['type'=>'divider'];

        $image = ['type'=>'image', 
                  'title'=>['type'=>'plain_text', 'text'=>'image1', 'emoji'=>true],
                  'image_url'=>'https://api.slack.com/img/blocks/bkb_template_images/beagle.png',
                  'alt_text'=>'image1',
        ];

        $element1 = ['type'=>'mrkdwn', 'text'=>'element1 :one:'];
        $element2 = ['type'=>'mrkdwn', 'text'=>'Last updated: Jan 1, 2019'];
        
        $context = ['type'=>'context', 'elements'=>[$element1,$element2] ];
        $payload['blocks']=[$section, $divider, $image, $divider, $context];

        //Guzzleならoptionにキー名'json'でPHP配列を渡すだけで、application/jsonヘッダーが送られる
        $response = $guzzle->post($api, ['json' => $payload]);

        $body = $response->getBody()->getContents();
        $json = json_decode($body, true);
        if( !$json['ok'] ){
            $this->info('slack returns error');
            $this->info($body);
            return 1;
        }else{
            $this->info('うまくいったようだぞ');
        }
        return 0;
    }
}

```

###実行
artisanコマンド経由で叩きましょう。

```:コマンドライン
php artisan slack:chat-post

うまくいったようだそ
```

これが出たら、Slackの指定のチャンネルにメッセージが届いているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/be4376ab-ec69-24e5-4ae8-0702c65afa97.png)


###トークンとas_userの関係について

postMessageの`as_user`

|No| token | as_user | 結果 |
|:--:|:---|:---|:---|
| 1|user token  | true  | Tokenの所有者としての発言になる。指定のChannelに参加していないとエラーになる  |
| 2|user token  | false  | Bot(App)の発言になる。指定のChannelに参加していなくても発言可能  |
| 3|bot token  | true  | Bot Userの発言になる。指定のChannelにAppが追加されていないとエラーになる  |
| 4|bot token  | false  | Bot(App)の発言になる。 指定のChannelに参加していなくても発言可能 |

:one:自分から送ったことになるのは、このパターンだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a79246f9-b9cf-1160-eb79-7e598041a999.png)
:two:ユーザートークンを使ってもAppからの発言です。その代わりChannelにいるか居ないかは影響しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/7c773634-1d7c-e694-68a9-beb34086a493.png)

:three:slalack-botuserになります。Appの中のBotUserから届きます。アイコンが同じなので気が付きにくいですけど。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/8d9f66d8-58c4-8610-3208-f595a5de1c12.png)
:four:２と同じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/41e5a9af-5189-6a85-d915-148114056005.png)

#まとめ
今回はSlackのWebAPIを使ってみました。APIは種類が多すぎて全部試すことはできないので2個だけですが、基本的な使い方と考え方は同じです。APIも単発で呼び出すことは少なく、APIを呼んだ結果を使って別のAPIを実行するみたいな組み合わせが大事です。

単発の機能説明ばかりやってきたので、次回は実用的なボットを作ってみます。