LoginSignup
330
150

More than 1 year has passed since last update.

ここがつらい! Slack API

Last updated at Posted at 2022-06-18
  • 半分ネタ記事です。あんまり真面目に書きません。
  • 項目数が多いので,気力でなんとか書きます。分類は諦めます。
  • 他にもある!っていうのがあったらコメント欄で教えて下さい。気が向いたら追記します。

公式の TypeScript 型定義がもはや型定義を諦めている

辛い度: ★★★★★

辛い中でもこれはかなり上位に来るやつ。

こちらに OpenAPI 形式で仕様が定義されていて,

ここに仕様に基づいて TypeScript の型定義ファイルが吐かれるようになっています。 Git 管理されていないので,実際のリリースを見てみましょう。

export declare type ReactionsGetResponse = WebAPICallResult & {
    channel?: string;
    error?: string;
    message?: Message;
    needed?: string;
    ok?: boolean;
    provided?: string;
    type?: string;
};
export interface Message {
    app_id?: string;
    blocks?: Block[];
    bot_id?: string;
    bot_profile?: BotProfile;
    permalink?: string;
    reactions?: Reaction[];
    team?: string;
    text?: string;
    ts?: string;
    type?: string;
    user?: string;
}
export interface Block {
    accessory?: Accessory;
    alt_text?: string;
    api_decoration_available?: boolean;
    block_id?: string;
    call?: Call;
    call_id?: string;
    dispatch_action?: boolean;
    element?: Accessory;
    elements?: Accessory[];
    external_id?: string;
    fallback?: string;
    fields?: Hint[];
    file?: File;
    file_id?: string;
    hint?: Hint;
    image_bytes?: number;
    image_height?: number;
    image_url?: string;
    image_width?: number;
    label?: Hint;
    optional?: boolean;
    source?: string;
    text?: Hint;
    title?: Hint;
    type?: string;
}

見てください!この undefined になりうる値の大海原!!!
最高すぎますよね!!!!!!!

全部のエンドポイントがこの状態です。どの値がどういう場合に省略されるのかは,実際にリクエストを送ってみて,返ってきたそれが答えです。

腐敗防止層を用意せずしてまともに起用する気にならない

辛い度: ★★★★☆

そのまま使ったら,アプリケーション側でしょっちゅう undefined でないかの確認が必要です。汚染がやばすぎる…

公式のドキュメントが間違っていることがある

辛い度: ★★★★★

辛かった場所が多すぎて何が間違っていたかあんまり覚えてないんですが,とにかくドキュメントは 100% は信じないほうがいいです。仕様よりも本物の動作が全て。

PHP 用の SDK が無い

辛い度: ★★★★★

これはポジショントークですが, PHP ユーザにとっては悲報です。公式が提供している SDK が

  • Java
  • Python
  • Node

以上 3 言語向けしかなく,マイナー言語である PHP は非公式のものを使わなければなりません。

ただ,これもコメントで触れられている通り,本家の仕様定義が間違っているために修正パッチを適用して生成したりしていることがあるので,マジで茨の道です。

開発やデバッグの手間がかかる

辛い度: ★★☆☆☆

実際に自分で動かさないとわからない!な Slack API ですが,シンプルな Web API (REST API と言いたいけど REST ではない)はローカルで動かすこともできるものの,スラッシュコマンドなど,ユーザ操作の介入を伴うものは Slack からアクセスできるように公開する必要があるため,ローカルでのデバッグが面倒臭いです。

今はこういうサービスもあるので,多少ラクにはなったかもしれません。

トークンの権限などの設定ページが難解・トークンの種類が多い

辛い度: ★★★☆☆

以下がトークンなどの設定ページです。

以下が最初に現れるページの抜粋スクショです。

image.png
image.png

さて,あなたは以下のような状況にいることを想定してください。

あなたは Slack を利用したアプリケーションの開発プロジェクトとして,後から参入してきました。既にいるメンバーが作ってくれたトークンが,新しい機能を開発するためには権限が足りませんでした。あなたはトークンに権限を追加する必要があります。

あなたはどこに行けばいいでしょうか?正解は…

image.png
image.png
image.png

状況(というか作る物)にもよりますが,殆どの場合の正解はこれです。途中で出てきましたが, Slack には 3種類 のトークンがあります。

Slack アプリで使用できるトークンには、ユーザートークン(xoxp)とボットトークン(xoxb)、アプリレベルトークン(xapp)の 3 種類があります。

  • ユーザートークン を使用すると、アプリをインストールまたは認証したユーザーに成り代わって API メソッドを呼び出すことができます。1 つのワークスペースに複数のユーザートークンが存在する可能性があります。
  • ボットトークン はボットユーザーに関連づけられ、1 つのワークスペースでは最初に誰かがそのアプリをインストールした際に一度だけ発行されます。どのユーザーがインストールを実行しても、アプリが使用するボットトークンは同じになります。 ほとんど のアプリで使用されるのは、ボットトークンです。
  • アプリレベルトークン は、全ての組織(とその配下のワークスペースでの個々のユーザーによるインストール)を横断して、あなたのアプリを代理するものです。アプリレベルトークンは、アプリの WebSocket コネクションを確立するためによく使われます。

更に,歴史的な部分を見ると, Bot トークンもトータルで3種類あったらしいです。

なんじゃそりゃ。どうしてこうなった。

API の種類が多すぎる

辛い度: ★★★★★

を見ると,以下の API が列挙されています。

  • Web API
  • Events API
  • Other APIs
    • Admin API
    • SCIM API
    • Audit Logs API
    • Status API
    • RTM API

(数えなくていいようなものもありますが)なんと合計 7 種類!
多くの場合は先頭 2 種類で済むよ〜と書いてくれているのが救いですね…

と言いたいのですが,なんと既にもうここでドキュメント漏れが存在します。以下の Interactivity API は完全にこのページからは漏れています。

図で整理してみると,こういう関係性。

(Events API だけどっちにも書いてあるので解釈がちょっと割れそう。性質的に,私はこれは Interactivity API とは別物だと考えています)
image.png

ややこしすぎるやろ!

Interactivity API の中で,Slash Commands だけ WebHook エンドポイントの管理方法とかペイロードフォーマットが違う

辛い度: ★★★☆☆

既に書いている通り, Slash Commands だけはコマンド1個毎に個別に登録する仕様になっています。歴史的に見ると,最初に Slash Commands が出来て,後から Shortcuts と Interactive Components が出来た影響で, Slash Commands だけ仕様がナンセンスになっていたりするのかなぁと予想。

そしてなんと, ペイロードフォーマットが…

API ペイロードフォーマット
Slash Commands application/x-www-form-urlencoded
Shortcuts application/x-www-form-urlencoded"payload" キー配下に URL エンコードされた application/json が存在
Interactive Components application/x-www-form-urlencoded"payload" キー配下に URL エンコードされた application/json が存在

なんでそうなったん?

Message Shortcut と Global Shortcut という分類なのにそれぞれ message_action shortcut という命名

辛い度: ★☆☆☆☆

Helps identify which type of interactive component sent the payload. Global shortcuts will return shortcut, message shortcuts will return message_action.

なんでそうなったん?

Slash Commands の引数パースを Slack 側がやってくれない

辛い度: ★★★★★

/weather 94070 を実行したときのペイロードがこちら。

token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/weather
&text=94070
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=13345224609.738474920.8088930838d88f008e0
&api_app_id=A123456

コマンドだけ command に分離されますが,残りの部分は全部 text に結合された状態で入ってきます。これを頑張ってパースしなければなりません,が…

ここに書かれている通り,数多くのエスケープ表現が存在します。メンションが典型的な例。ライブラリ無しでは正直やってられません…

(PHP 製のまともなライブラリが1つも無かったので,業務用にガッツリ書いて作ってます。まだ OSS 化は出来ていませんが)

Block Kit システムでのレイアウトが3種類ある

辛い度: ★★★★★

Interactive Components の構成要素として, 「モーダル」「メッセージ」「ホームタブ」の 3 種類があることを既に示しましたが,これらで実現可能なことが若干違っています。モーダルが持つ機能が多いです。

アクションタイプ 対応範囲 説明
Block Action モーダル
メッセージ
ホームタブ
あらゆるコントロールを触った瞬間に発生するアクション
View Submission モーダル モーダルのサブミットボタン押下で送信するアクション

そのほか,モーダルだけ private_metadata というフィールドに状態を保持したり,モーダルの上にモーダルをスタックとして積んで開いたり,開発者の対応が死ぬほど面倒くさい器用なことが出来たりします。

Block Kit Builder に状態管理まわりの不具合があって仕様を知らないと誤解しやすい

辛い度: ★★☆☆☆

↑が Block Kit Builder というツールで,視覚的に Interactive Components を組み立てられる優れものです。これ自体はめちゃくちゃ便利なのですが…

「Actions Preview」タブでペイロードの中身を確認するとき, 直近の1アクションで発生したものしか state.values の中に現れなくなっています。言葉よりも実際の状態を見てもらったほうがいいのでスクリーンショットを。

image.png

これは

  1. テキストボックスに内容を入力
  2. チェックボックスをチェック
  3. チェックボックスをチェックした瞬間の送信アクションを確認

という手順で調べたものですが,テキストボックスの中身を入れたはずなのになぜか null になっています。これは Block Kit Builder の仕様で,実際はちゃんと値は入ってきます。この仕様を知らないと無駄に悩むことになると思うので,ここで覚えておいてください。

Block Kit で見た目が同じなのに分類や JSON ペイロードの持ち方が全く違うコントロールがある

辛い度: ★★★☆☆

Block Kit Builder の左側のペインを見てもらうと分かりますが,同じような名前のものが違うグルーピングのところに存在します。

image.png

これはどちらもラジオボタンですが,それぞれ

  • Input with Radio Buttons
  • Section with Radio Buttons

で微妙なデザイン差異や JSON ペイロードの構造に違いがあります。どっちかだけで良くないかこれ…?

Interactive Components で応答するときに「WebHook レスポンスに載せていいもの」「別途リクエストを送る必要があるもの」の区別がつきにくい

辛い度: ★★★★☆

以下に「WebHook のレスポンスとして 200 OK で返していいもの」を示します。

Slash Commands Block Actions View Submissions Message Shortcuts Global Shortcuts
空レスポンスによる ACK 他を選択しない場合必須 必須 他を選択しない場合必須 必須 必須
メッセージ応答 任意 :x: :x: :x: :x:
モーダル状態コントロール :x: :x: 任意 :x: :x:

ここの「Sending an immediate response」の部分を見て「どうせモーダルでも一緒だろ」と思い込んで,モーダルからメッセージ応答しようとしたら全く動かなかったのが苦い思い出。

response_url という分かりづらい命名の機能があるが, Qiita に Slack の中の人が解説記事をわざわざ投稿するぐらい難解

辛い度: ★★★★★

実は各インタラクションには response_url というフィールドが含まれているのですが,これに対して更にこちらからリクエストを送信するとACK を送った後に別途遅延処理としてメッセージ応答を伝えることができます。↑で同期的なレスポンスで返せなかった Slash Commands 以外のインタラクションも,これを使えば対応できたりします。

…が,これの仕様がとにかく難解。 なんと Slack の中の人が Qiita に解説記事を投稿しています。

記事はとてもありがたいけど,もうちょっとこう…簡単にならないんですかねぇ…w

Web API の エンドポイントの命名が REST じゃない

辛い度: ★★☆☆☆

命名で Web API という形でごまかされています。本来は REST API と書きたい部分だと思われますが…

例↓

用途 メソッド パス
メッセージの作成 POST chat.postMessage
メッセージの削除 POST chat.delete
投稿へのリアクションの追加 POST reactions.add
投稿からのリアクションの削除 POST reactions.remove
ある投稿に対するリアクション群の取得 GET reactions.get
あるユーザが行ったリアクション群の取得 GET reactions.list

これだけで「ん???」と感じられる方はいらっしゃると思いますが,実際そうなってます。動いているものが正義なんです。

(REST は万能ではないので至上主義ではないにしろ,それにしても命名をもうちょっとしっかりやってほしいですね…)

ステータスコードが殆どすべて 200 で,存在しないエンドポイントにリクエストしても 200 が返ってくる

辛い度: ★☆☆☆☆

失敗したときは {"ok":false} で返してくるタイプの API です。まあ REST じゃないって言ってるからこれは許してあげよう。

user@host:~$ curl -i -X POST https://slack.com/api/chat.postMessage
HTTP/2 200
content-type: application/json; charset=utf-8
{"ok":false,"error":"not_authed"}

user@host:~$ curl -i -X GET https://slack.com/api/foo.bar
HTTP/2 200
content-type: application/json; charset=utf-8
{"ok":false,"error":"unknown_method","req_method":"foo.bar"}

user@host:~$ curl -i -X GET https://slack.com/api/foo-bar
HTTP/2 404
content-type: text/html; charset=utf-8

だが存在しないエンドポイントも 200 で返してくるのどうなん!?
しかし,ルーティングに引っ掛からないような文字種を使うと一応 404 になるらしい…なんじゃこりゃ

メッセージを特定するための ID 相当のものが「チャンネル」「タイムスタンプ」の複合ナチュラルキーになっている

辛い度: ★★★★☆

殆どの方は, Web アプリケーションの開発には

  • AUTO_INCREMENT SERIAL IDENTITY と呼ばれる自動採番値
  • UUID と呼ばれる,確率的にほぼ被らないことが保証されている仕様の値

のいずれかをサロゲートキー(値自体に意味は無い,整理のために発行されるキー)を使われると思いますが, Slack は一部が複合ナチュラルキー(意味のある値の組み合わせのキー)になっています。メッセージに割り振られるものは複合ナチュラルキーです。ナチュラルキーそのものは否定しませんが,複合になってくると管理がそれなりに面倒です。

キー 説明 具体例
channel_id チャンネルの ID
(これ自体はサロゲートキー)
C0NF841BK
timestamp メッセージが投稿されたときのマイクロ秒精度のタイムスタンプ 1524523204.000192

これらの値を2つとも渡さないとメッセージを特定できないケースが殆どです。

channel_id は,そのトークンでアクセス可能なワークスペース単位でしかユニークではありません。別の会社の Slack に,全く同じ channel_id が存在している可能性はあります。

もし腐敗防止層をアプリケーション側で用意する場合は,これらの値をセットにしてシリアライズすると設計がラクになるでしょう。

「投稿の詳細情報を取得」に使うエンドポイントが事実上 reactions.get になる

辛い度: ★★★★☆

ID を指定して投稿の詳細情報を取得!と思い立ったら messages.get messages.show あたりを考えると思いますが,正解は reactions.get です。

仕様をよく読んでみると…

  • 渡す値
    • メッセージのリアクションを取得する場合, channel timestamp を渡す
    • ファイルのリアクションを取得する場合, file を渡す
    • ファイルコメントのリアクションを取得する場合, file file_comment を渡す
  • 返り値
    • 取りに行ったリアクション対象そのものの中に reactions というキーでリアクション群が内包されるようになっている

という形になっていることが分かります。つまり reactions.get の実態は messages.get?include=reactions に近いんです。

full パラメータを付けないと,リアクションの取得上限は 50 となります。完全な情報が欲しい場合は付けましょう。

メッセージの場合は,以下のようなペイロードとして取得されます。

普通のメッセージ
{
  "ok": true,
  "type": "message",
  "message": {
    "type": "message",
    "text": "test",
    "user": "U02ASEM3ESV",
    "ts": "1640154436.002000",
    "team": "T02FKA4M391",
    "permalink": "https://.../archives/C02S0JR88AD/p1640154436002000"
  },
  "channel": "C02S0JR88AD"
}
リアクションが1件着いたメッセージ
{
  "ok": true,
  "type": "message",
  "message": {
    "type": "message",
    "text": "test",
    "user": "U02ASEM3ESV",
    "ts": "1640154436.002000",
    "team": "T02FKA4M391",
    "reactions": [
      {
        "name": ":ok:",
        "users": [
          "U01V7FUJWC8"
        ],
        "count": 1
      }
    ],
    "permalink": "https://.../archives/C02S0JR88AD/p1640154436002000"
  },
  "channel": "C02S0JR88AD"
}

そして,リアクションがあるときだけフィールドが出現します!さすが何でもあり undefined な Slack API。

また,リアクションに加えて パーマリンク も含まれている点も重要だったりします。その理由は後述します。

スレッドの表現形式がメッセージの親子関係になっている

辛い度: ★★★★☆

reactions.get を使って,スレッド内に作られたメッセージを取得すると以下のようになります。

スレッド内に作られたメッセージ
{
  "ok": true,
  "type": "message",
  "message": {
    "type": "message",
    "text": "スレッド内メッセージです",
    "user": "U02ASEM3ESV",
    "ts": "1642505773.002100",
    "team": "T02FKA4M391",
    "thread_ts": "1642504946.001400",
    "parent_user_id": "U02ASEM3ESV",
    "permalink": "https://.../archives/C02S0JR88AD/p1642505773002100?thread_ts=1642504946.001400&cid=C02S0JR88AD"
  },
  "channel": "C02S0JR88AD"
}

なにやら thread_ts parent_user_id というものが生えてきました!

キー スレッドに属すとき
必ず存在するか?
説明
thread_ts YES 所属対象のスレッド化されたメッセージのタイムスタンプ
parent_user_id NO 所属対象のスレッド化されたメッセージの作成者 ID。所属対象のメッセージ作成者が「自分自身」「Bot」のいずれかに該当する場合は存在しない

スレッドとして全く別の第 3 のオブジェクトが作られるわけではなく,他のメッセージを parent として親に持つ仕様になっていることに注意してください。

スレッドに属するメッセージであるかを判定するためには,以下のようにタイムスタンプを比較検証します。

const isReplyMessage = typeof message.thread_ts === 'string'
                    && message.ts !== message.thread_ts

image.png
↑デバッガーの苦しみ

返信された瞬間にメッセージはスレッド扱いになり,未定義だったフィールドが後から生える

辛い度: ★★★★★

他のメッセージが返信として付いて,スレッド化されたメッセージ
{
  "ok": true,
  "type": "message",
  "message": {
    "type": "message",
    "text": "親メッセージ",
    "user": "U02ASEM3ESV",
    "ts": "1642504946.001400",
    "team": "T02FKA4M391",
    "thread_ts": "1642504946.001400",
    "reply_count": 1,
    "reply_users_count": 1,
    "latest_reply": "1642506002.004200",
    "reply_users": [
      "U01V7FUJWC8"
    ],
    "is_locked": false,
    "subscribed": false,
    "permalink": "https://.../archives/C02S0JR88AD/p1642504946001400?thread_ts=1642504946.001400&cid=C02S0JR88AD"
  },
  "channel": "C02S0JR88AD"
}

もうどんなペイロードを見てもあなたは驚かないでしょう。これが Slack API です。

キー スレッド化されたとき
必ず存在するか?
説明
thread_ts YES メッセージのタイムスタンプ
reply_count YES 返信数
reply_users_count YES 返信者数
latest_reply YES 最も新しい返信のタイムスタンプ

スレッド化されたメッセージであるかどうかを判定するためには,以下のようにタイムスタンプを比較検証します。
(こちらは他の方法もありますが,返信と対象的な書き方をしたほうが良いでしょう)

const isThreadRootMessage = typeof message.thread_ts === 'string'
                         && message.ts === message.thread_ts

全ての返信が削除されると,自分自身は再びスレッドでは無くなります。ステートフルなので気をつけてください!

返信が残った状態で所属先のスレッドだけを削除すると,削除したメッセージが Slack Bot の投稿に変身する

辛い度: ★★★★☆

お前は何を言っているんだ感がありますが,これです,これ。見たことありますよね。

image.png

これを API で取得するとこうなっています。

{
  "ok": true,
  "type": "message",
  "message": {
    "type": "message",
    "subtype": "tombstone",
    "text": "This message was deleted.",
    "user": "USLACKBOT",
    "hidden": true,
    "ts": "1655549063.053909",
    "thread_ts": "1655549063.053909",
    "parent_user_id": "USLACKBOT",
    "reply_count": 1,
    "reply_users_count": 1,
    "latest_reply": "1655549413.855839",
    "reply_users": [
      "U02ASEM3ESV"
    ],
    "is_locked": false,
    "subscribed": false,
    "permalink": "https://.../archives/C02S0JR88AD/p1655549063053909?thread_ts=1655549063.053909&cid=C02S0JR88AD"
  },
  "channel": "C02S0JR88AD"
}

USLACKBOT公式 Slack Bot 的なものです。この子です。
image.png
アイコン違うやん!ゴミ箱やん!
(てかサブタイプ tombstone なのに墓石じゃないんや…)

パーマリンクを取得するためのエンドポイントがわざわざ存在する

辛い度: ★★★☆☆

また,リアクションに加えて パーマリンク も含まれている点も重要だったりします。その理由は後述します。

と書きましたが,その理由は パーマリンクが含まれないエンドポイントがある からです。

通常メッセージまたはスレッドルートメッセージのパーマリンク
# チャンネルID/タイムスタンプ
https://.../archives/C02S0JR88AD/p1642505773002100
スレッドに所属する返信メッセージのパーマリンク
# チャンネルID/タイムスタンプ?thread_ts=スレッドルートのタイムスタンプ&cid=スレッドルートのチャンネルID
https://.../archives/C02S0JR88AD/p1642505773002100?thread_ts=1642504946.001400&cid=C02S0JR88AD

パーマリンクは上記のようなフォーマットとなりますが,ワークスペースの名前やスレッドの親子関係も含ませる必要があるため,自前生成はかなり骨が折れます。これは無いと困りますね。

パーマリンクを持たないレスポンスを返してくるエンドポイントの一例を挙げます。

conversations.history conversations.replies reaction_added event
permalink を含む? :x: :x: :x:
thread_ts を含む? :x:

これで最も困るのは, reaction_added event です。なんと, Event API で入ってくる値は返信メッセージであっても thread_ts を含まないため, 親を特定することができません。せめてパーマリンクさえあれば特定できるのに…

という流れで(知らんけど?),パーマリンクだけを取得するエンドポイントがこの悲劇を救済するかのように生えています。

{
    "ok": true,
    "channel": "C1H9RESGA",
    "permalink": "https://.../archives/C1H9RESGA/p135854651500008"
}

本当に最小の情報しか返さないんだな…

…しかし…お気づきでしょうか?reactions.get を使うとパーマリンクやその他もろもろの情報を根こそぎ全部取れることに! 但し,設定されている Tier が異なるため,可能な限り負荷の軽いほうを使う方が良いでしょう。

エンドポイント Tier 1分あたりの上限アクセス数(概数)
reactions.get 3 50
chat.getPermalink 特殊 ほぼ無制限

reactions.get を投稿の詳細情報取得に使用, chat.getPermalink を投稿の親子関係取得に使用… 命名からは想像もつかない濫用だ…

「Bot」「組織外ユーザ」「ゲスト」「オーナー」などの権限判定方法が非常に難解

辛い度: ★★★★★

権限を管理するのはどんなアプリケーションでも骨が折れるトピックですが, Slack の内部はかなり大変なことになっているようです。以前整理してみたときの樹形図があるので掲載します。

image.png

(一部私が考えた造語を使っています)

  • Bot は SlackBot(公式のUSLACKBOT) と CustomBot(ユーザが作るもの)の2種類がある
  • 通常ユーザは組織内しか見えないが, Slack Connect で繋がっている場合は外部でも見える
  • 組織内ユーザは,通常ユーザとは別に招待できるゲストがあり, シングルチャンネルゲストマルチチャンネルゲスト の2種類がある
  • 組織内ユーザのうち一部は強い権限を持ち,強い順に 代表オーナー, オーナー, 管理者 の3種類がある

いやーめちゃくちゃややこしいですね。そしてこれらを完全に判定・分離するロジックがこちら。

PHP コード
class Member
{
    private const SLACK_BOT_ID = 'USLACKBOT';

    // ObjsUser は SlackAPI のレスポンスほぼそのまま
    // jolicode/slack-php-api が提供するクラス
    public static function fromApiUser(ObjsUser $user): self
    {
        $id = $user->getId();

        assert($id !== null);

        return new self(
            id: $id,
            email: $user->getProfile()?->getEmail(),
            isBot: $user->getIsBot() ?? false,
            isAdmin: $user->getIsAdmin() ?? false,
            isOwner: $user->getIsOwner() ?? false,
            isPrimaryOwner: $user->getIsPrimaryOwner() ?? false,
            isRestricted: $user->getIsRestricted() ?? false,
            isUltraRestricted: $user->getIsUltraRestricted() ?? false,
        );
    }

    public function __construct(
        private readonly string $id,
        private readonly ?string $email,
        private readonly bool $isBot,
        private readonly bool $isAdmin,
        private readonly bool $isOwner,
        private readonly bool $isPrimaryOwner,
        private readonly bool $isRestricted,
        private readonly bool $isUltraRestricted,
    ) {
    }

    public function isBot(): bool
    {
        return $this->isSlackBot() || $this->isCustomBot();
    }

    public function isSlackBot(): bool
    {
        return $this->id === self::SLACK_BOT_ID;
    }

    public function isCustomBot(): bool
    {
        return $this->isBot;
    }

    public function isHuman(): bool
    {
        return !$this->isBot();
    }

    public function isHumanInExternalOrganization(): bool
    {
        // NOTE:
        //   外部ユーザは「users:read.email スコープを付与してもメールを取得できない」で判断
        //   https://engineer.dena.com/posts/2020.11/slack-app-for-enterprise-grid/
        return $this->isHuman()
            && $this->email === null;
    }

    public function isHumanInInternalOrganization(): bool
    {
        return $this->isHuman()
            && $this->email !== null;
    }

    public function isGuest(): bool
    {
        return $this->isRestricted;
    }

    public function isSingleChannelGuest(): bool
    {
        return $this->isUltraRestricted;
    }

    public function isMultiChannelGuest(): bool
    {
        return $this->isGuest() && !$this->isSingleChannelGuest();
    }

    public function isRegularMember(): bool
    {
        // NOTE:
        //   トークンに users:read.email スコープが無いと完全にバグるので注意
        return $this->isHumanInInternalOrganization()
            && !$this->isGuest();
    }

    public function isPrivilegedRegularMember(): bool
    {
        return $this->isRegularMember() && ($this->isAdmin() || $this->isOwner());
    }

    public function isNonPrivilegedRegularMember(): bool
    {
        return $this->isRegularMember() && (!$this->isAdmin() && !$this->isOwner());
    }

    public function isAdmin(): bool
    {
        return $this->isAdmin;
    }

    public function isOwner(): bool
    {
        return $this->isOwner;
    }

    public function isPrimaryOwner(): bool
    {
        return $this->isPrimaryOwner;
    }

    public function isSecondaryOwner(): bool
    {
        return $this->isOwner() && !$this->isPrimaryOwner();
    }
}
              ,, -―-、
             /     ヽ
       / ̄ ̄/  /i⌒ヽ、|    オエーー!!!!
      /  (゜)/   / /
     /     ト、.,../ ,ー-、
    =彳      \\‘゚。、` ヽ。、o
    /          \\゚。、。、o
   /         /⌒ ヽ ヽU  o
   /         │   `ヽU ∴l
  │         │     U :l
                    |:!
                    U

is_restricted is_ultra_restricted とかいう謎の命名と,外部組織ユーザの判定ロジックが香ばしいポイント。以下の記事は大変参考になりました。

歴史的理由で /me コマンド専用の謎のメッセージ形式がある

辛い度: ★★☆☆☆

image.png

ん?なんやこれ?

image.png

API で取ってみると,中身は全く違います。

[
  {
    "type": "message",
    "text": "_これは書式変更で斜体にしただけ_",
    "user": "U02ASEM3ESV",
    "ts": "1655561762.619899",
    "team": "T02FKA4M391",
    "blocks": [
      {
        "type": "rich_text",
        "block_id": "Wn6",
        "elements": [
          {
            "type": "rich_text_section",
            "elements": [
              {
                "type": "text",
                "text": "これは書式変更で斜体にしただけ",
                "style": {
                  "italic": true
                }
              }
            ]
          }
        ]
      }
    ]
  },
  {
    "type": "message",
    "subtype": "me_message",
    "text": "これは /me コマンド",
    "user": "U02ASEM3ESV",
    "ts": "1655561735.831039"
  }
]

斜体に見えるだけの謎のメッセージタイプ。なぜこんなものがあるんでしょうか?以下,調べてみた結果

教えていただいたツイートが発掘できなかったのですが,マイクラのお遊びコマンドが元ネタのようで…

例えば /me left the channel のように書くと,自分が退室したことに関するシステムアナウンスのように偽装できたようです。

Slack 「ビジネス展開ガチで進めてきたし,そろそろ黎明期のお遊びコマンドは消すか。真面目な場で悪用されたらヤバいし」
ユーザ 「えええええええええええええええ!!!!突然の破壊的変更はやめてくれ!!!!復活させろや!!!!」
Slack 「ごめんて。完全に消すのはアレやからとりあえず斜体で出すだけのコマンドとして復活させとくわ。ほなおおきに」

(きっとこういう流れ)

JSON ペイロードの中の似ている同士の情報のどちらを使ったらいいのか分かりにくいものがある

辛い度: ★★★☆☆

名前に関する項目を絞り込むと,似たようなフィールドがこれだけあります。上の記事を読んでいてもなおまだ迷いますね…

{
  "name": "spengler",
  "real_name": "Egon Spengler",
  "profile": {
    "real_name": "Egon Spengler",
    "display_name": "spengler",
    "real_name_normalized": "Egon Spengler",
    "display_name_normalized": "spengler",
  },
}
  • *_normalized はラテン文字を変換したやつかな…?
  • real_name はダブってるけどどっちを見ればいいんですかねぇ…
    • 多分 profile が後から追加されたので profile.real_name を使うべきと予想

なお,名前以外にもまだ何個かダブり項目はあったような気がします…

330
150
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
330
150