SlackのAPIで遊んでみる

#はじめに
この記事はトラストバンクアドベントカレンダー13日目の記事です。

こんにちは！トラストバンクのテックリード 海塩（うしお）（@youhei_seasalt）と申します。
会社として初の試みであるアドベントカレンダーですが、毎日同僚の記事が提供されるというのはとても楽しいですね。

#なにをしたか
最近SlackのAPIを触ることがあったため、リアクションを集計して遊びました。
一番多く使われたリアクションや、一番多くリアクションを受けた人を集計できます。
Slack上でおもむろに↓こんな発言をしようとしています。
![reaction1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124294/5e2488e9-8dac-505e-4a98-d085b94c18ff.png)
GASのほうが親和性が高く、ネット上のサンプルも多いですがPHPerなのであえてPHPでやってみます。

#APIを探す
公式のAPIページを漁ります。
このAPIでメッセージの履歴が取得できそうです。
https://api.slack.com/methods/conversations.history



#APIトークンはどこで？
まずはSlack APIを使えるようにしなければいけません。
いつものように「slack api 使い方」でググります。
いくつかページを見て、Slackアプリを作ればいけそうとわかりました。
早速作ります。
権限はこのくらい付けておけばいいでしょう。
![permission.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124294/456f1b18-bd97-7c49-a6c9-9752d20a25db.png)


#APIを呼んでみる
マニュアルみながらコードを組んでみます。

```PHP
$client = new \GuzzleHttp\Client();
$token = 'xoxp-xxxxx-xxxxx-xxxxx......';
$response = $client->request(
    method: 'GET',
    uri: 'https://slack.com/api/conversations.history',
    options: [
        'headers' => [
            'Authorization' => 'Bearer ' . $token,
            'Content-Type' => 'application/json',
        ],
        'query' => [
            'channel' => 'xxxxxxx',
            'oldest' => strtotime('2021-11-01 00:00:00'),
            'latest' => strtotime('2021-11-30 23:59:59'),
        ],
        'timeout' => 120,
    ]);
$responseBody = json_decode(
    json: $response->getBody()->getContents(),
    associative: true);
```

取得できたようです。
`$responseBody['messages']`にメッセージが入っています。
`$responseBody['messages'][n]['reactions']`にリアクションも入っていますね。

#集計用のデータを蓄積
いろんなパターンで集計したいので、いったんMySQLにデータを蓄積します。
Laravelを使っているのでマイグレーションを作ります。

```PHP
return new class() extends Migration
{
    public function up()
    {
        Schema::create('slack_reactions', function (Blueprint $table) {
            $table->id();
            $table->string('name', 100)->comment('絵文字の名前');
            $table->string('sender_id', 15)->comment('リアクションを行ったユーザのID');
            $table->string('recipient_id', 15)->comment('リアクションをもらったユーザのID');
            $table->string('message_id', 100)->comment('メッセージID');
            $table->dateTime('message_date')->comment('メッセージ日時');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('slack_reactions');
    }
};
```

続いて、APIで取得したデータを保存していきます。

```PHP
foreach ($responseBody['messages'] ?? [] as $message) {
    if (!isset($message['user'])) {
        // BOTの発言かな？　発言者のIDがとれないので無視
        continue;
    }
    foreach ($message['reactions'] ?? [] as $reaction) {
        foreach ($reaction['users'] as $reactionUser) {
            $reactions[] = [
                'name' => $reaction['name'],
                'sender_id' => $reactionUser,
                'recipient_id' => $message['user'],
                'message_id' => $message['client_msg_id'] ?? '',
                'message_date' => date('Y-m-d H:i:s', intval($message['ts'])),
            ];
        }
    }
}
DB::table('slack_reactions')->insert($reactions);
```

ちゃんと保存されましたが、件数がぜんぜん足りません。
マニュアルよく見ると、limitがデフォルトで100でした。
すべて取得したい場合はカーソルを使って取得を繰り返すようです。

```PHP
/**
 * メッセージ履歴を取得する
 *
 * @param string|null $cursor
 * @return array
 * @throws \GuzzleHttp\Exception\GuzzleException
 */
private function fetchHistories(string|null $cursor): array
{
    $client = new \GuzzleHttp\Client();
    $token = 'xoxp-xxxxx-xxxxx-xxxxx......';
    $response = $client->request(
        method: 'GET',
        uri: 'https://slack.com/api/conversations.history',
        options: [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            'query' => [
                'cursor' => $cursor,
                'channel' => 'xxxxxxx',
                'oldest' => strtotime('2021-11-01 00:00:00'),
                'latest' => strtotime('2021-11-30 23:59:59'),
            ],
            'timeout' => 120,
        ]);
    return json_decode(
        json: $response->getBody()->getContents(),
        associative: true);
}
```

メソッド化して、繰り返し呼びます。

```PHP
$responseBody = $this->fetchHistories(null);
while (count($responseBody['messages'] ?? []) > 0) {
    foreach ($responseBody['messages'] as $message) {
        if (!isset($message['user'])) {
            // BOTの発言かな？　発言者のIDがとれないので無視
            continue;
        }
        foreach ($message['reactions'] ?? [] as $reaction) {
            foreach ($reaction['users'] as $reactionUser) {
                $reactions[] = [
                    'name' => $reaction['name'],
                    'sender_id' => $reactionUser,
                    'recipient_id' => $message['user'],
                    'message_id' => $message['client_msg_id'] ?? '',
                    'message_date' => date('Y-m-d H:i:s', intval($message['ts'])),
                ];
            }
        }
    }
    if (!isset($responseBody['response_metadata']['next_cursor'])) {
        break;
    }
    $responseBody = $this->fetchHistories($responseBody['response_metadata']['next_cursor']);
}

DB::table('slack_reactions')->insert($reactions);
```

ちゃんと１ヶ月分とれました。

#集計
多く使われたリアクションをSQLで出してみます。
コピペしたいので結果をちょっと整形しています。

```SQL
SELECT
    CONCAT(ROW_NUMBER() OVER (ORDER BY total DESC), '位') AS number,
    CONCAT(ranking.total, '回') AS total,
    ranking.reaction
FROM (
    SELECT COUNT(*) AS total,
         CONCAT(':', slack_reactions.name, ':') AS reaction
    FROM slack_reactions
    GROUP BY reaction
    ORDER BY total DESC
    LIMIT 10
) AS ranking
```

#人の名前をとりたい

JOINしたいのでテーブルを用意します。

```PHP
Schema::create('slack_users', function (Blueprint $table) {
    $table->id();
    $table->string('slack_user_id', 15)->unique()->comment('Slack上でのユーザID');
    $table->string('first_name', 100)->comment('名');
    $table->string('last_name', 100)->comment('姓');
    $table->string('email', 100)->unique()->comment('メールアドレス');
    $table->timestamps();
});
```

ユーザの取得APIは
[https://api.slack.com/methods/users.list](https://api.slack.com/methods/users.list)
ですね。

#もう一度集計
リアクションを一番もらった人を集計します。

```SQL
SELECT
    CONCAT(ROW_NUMBER() OVER (ORDER BY total DESC), '位') AS number,
    CONCAT(ranking.total, '回') AS total,
    ranking.username
FROM (
     SELECT COUNT(*) as total,
            CONCAT(recipient.first_name, recipient.last_name) AS username
     FROM slack_reactions
              INNER JOIN slack_users AS recipient ON slack_reactions.recipient_id = recipient.slack_user_id
     GROUP BY username
     ORDER BY total DESC
     LIMIT 10
 ) AS ranking
```
とれました。名前は出せないですが、開発リーダーがCTOを抑えて1位でした。
~~わたしですか？６位です。まあまあですね~~

#さいごに
今回は開発チームのチャンネルでやってみましたが、他にも色々できそうです。
たとえば会社では[Colla](https://colla.jp/)というアプリを導入しているのですが、なかなか面白い質問と回答があります。
リモートワークなのでこういった遊びをどんどん盛り込んでコミュニケーションを増やしていきたいですね。