Slack の絵文字がどれくらい使われているか集計するツール

Slack の Web API はわりといろいろある

api.slack.com

30 種類ものメソッドがあるぞ! と紹介されている. しかしながら,emoji 関連の API に関しては貧弱で emoji.list しかない. これは単純に登録されている emoji と画像のマップを取得するもの.

だから,チャンネルでどれくらい emoji が使われているか,とかは API 経由で集計できないという問題がある.

emoji の API がないのが問題か,と言われればまったく問題ではないとは思う.

結論

ゴリって集計する低機能なツールができた:

github.com

きっかけ

前職を退職するときに,戯れに会社のワークスペースでどの emoji が一番使われているのだろう,と気になったことがあった.

というのも,当初のワークスペースはそんなに emoji がなくて,誰かがよくある4文字のリアクション emoji が追加され,それに乗っかって「草」とか「LGTM」とかそういう emoji を追加したりした,時が経ちいろいろな人が同様に追加して emoji が繁栄するようになった.

自分の分報チャンネルで実験的なことをして結果を出すのが楽しかった時代があり,そういう中で,最後にどれくらい使われていたんだろうか集計して発表したいと気になったのだった.

で,結果としては「ありがとうございます」が最も使われていて,ついで感謝系の絵文字や「草」等の汎用性ある感じのリアクションがよく使われていた.これも会社の特色とかが出るだろうな〜と思ったりした.

ツール

閑話休題で,先程の Slack の Web API の話に戻ってくるが,emoji の使用量を取るような API はないので,自分でチャンネルのメッセージをゴリゴリとクロールして集計するしかない,という結論になった.

そのときのツールが残ってるかな? と思いきや,どこにもコード片が残っておらず,せっかく作ったのだから残しておきたかったな〜と思った.が,大したものでもないので再実装するか,と再度作ることになった.

流れ

ツールでやることはすごい単純で力技:

  1. conversations.listワークスペースのチャンネルリストをとってくる
  2. converstations.history であるチャンネルのメッセージを新→旧にページングしつつ取得してくる
  3. メッセージに emoji リアクションが含まれれば,展開する
  4. 展開した emoji をひたすら保存していく
  5. 最後に使用回数の多い順にソートして出力する

そして,Web APInpm install @slack/web-api で系統だったものが手に入るので,ぜんぜん難しくない.

クロールはチャンネルの長さによるがそれなりに時間がかかる.というのも,API 自体のクオータがあるし,直列でゴリゴリと取ってくるのでどうしても時間がかかってしまう.

前述したライブラリはリクエスト制限にひっかかるとよしなにしてくれるので,実装的にはクライアント側で気にするものでもないが,やはり非効率的.

集計

前職でやったときは普通に Map<string, Emoji> 的なものにゴリゴリと入れていって,あとでソートしたが,クロールした結果が永続化できなくてもったいなかったので,今回は SQLite で残そうとした.

そうすればチャンネル別とかこの人はこの emoji をよくつけられている,とかそういう集計もしやすいと思ったからだ.

極端な話ローカル側にすべてあれば集計は適当な方法でできるのでとにかく永続化できればよい.

ところでこのライブラリが Promise 使えてマイグレーションもあるので使いやすい:

www.npmjs.com

課題

Slack の Web API のリファレンスを見ていて気づいたのが,メッセージに対して「だれがこの emoji をつけたか」リストが取得できるのだが,このリストは全員を含んでいるものではない,という事実がある.

api.slack.com

The users array in the reactions property might not always contain all users that have reacted (we limit it to X users, and X might change), however count will always represent the count of all users who made that reaction (i.e. it may be greater than users.length).

なのでリアクションのカウントが 50 とかあっても,そのリストには 5 人分の ID しか含まれていない,とかそういうことがありうる,ということだ (この制限は X 人で非公開で,X は変更されるかもしれない).

「この人はよくこの emoji を使う」とか,そういうことが分かれば面白かったのだが,完璧にはわからなそうで,今回は「だれが」は集計しないことにした.

ページネーション

前回から地味に進化していて,ページネーションを AsyncIterableIterator で取得できるようになっていた.

slack.dev

だから,ページネーションを自前でしなくても,そのまま for await ... of で取得できるようなっている:

import { WebAPICallResult } from '@slack/web-api';

const pageIterator = this.client.paginate('conversations.list') as AsyncIterableIterator<WebAPICallResult>;
for await (const page of pageIterator) {
    console.log(page);
}

以前は whileAsyncGenerator を使って自前でページネーションをしていた:

async *fetchChannelPages(): AsyncGenerator<WebAPICallResult> {
    let cursor: string | undefined = undefined;
    do {
        const result: WebAPICallResult = await this.client.conversations.list({ cursor });
        if (result) {
            yield result;
        }
        cursor = result.response_metadata?.next_cursor;
    } while (cursor);
}

覚えてないけどこういう感じ.


ただ,現時点ではこのコードは TypeScript ではそのまま動かないようになっている.

というのも,型定義で AsyncIterator が返ってくると定義されていて,実際は AsyncGenerator が返ってきているから.

github.com

これは Pull Request が出ていて,これがマージされれば OK になるだろう.Pull Request では AsyncIterableIterator となっているが,実際のコードは generator を使ってるので AsyncGenerator でもいける,と思う.

マージされるまでの間は as AsyncGenerator<...> or as AsyncIterableIterator<...> にキャストする Workaround を使うことができる.

コード

github.com

試しに自分のワークスペースを作って少ない分量で試して満足して書き捨てた. 会社の長い歴史のあるワークスペースだとうまく行かないかもしれない.

前回は前職の1年くらいしか歴史のないワークスペースで, 全チャンネルのクロールが現実的な時間で完了した記憶があるので, ある程度なら大丈夫じゃないかと思っている.