- 半分ネタ記事です。あんまり真面目に書きません。
- 項目数が多いので,気力でなんとか書きます。分類は諦めます。
- 他にもある!っていうのがあったらコメント欄で教えて下さい。
気が向いたら追記します。
公式の TypeScript 型定義がもはや型定義を諦めている
辛い度: ★★★★★
辛い中でもこれはかなり上位に来るやつ。
こちらに OpenAPI 形式で仕様が定義されていて,
ここに仕様に基づいて TypeScript の型定義ファイルが吐かれるようになっています。 Git 管理されていないので,実際のリリースを見てみましょう。
- https://unpkg.com/@slack/web-api@6.7.2/dist/response/ReactionsGetResponse.d.ts
- https://unpkg.com/@slack/web-api@6.7.2/dist/response/ConversationsHistoryResponse.d.ts
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 からアクセスできるように公開する必要があるため,ローカルでのデバッグが面倒臭いです。
今はこういうサービスもあるので,多少ラクにはなったかもしれません。
トークンの権限などの設定ページが難解・トークンの種類が多い
辛い度: ★★★☆☆
以下がトークンなどの設定ページです。
以下が最初に現れるページの抜粋スクショです。
さて,あなたは以下のような状況にいることを想定してください。
あなたは Slack を利用したアプリケーションの開発プロジェクトとして,後から参入してきました。既にいるメンバーが作ってくれたトークンが,新しい機能を開発するためには権限が足りませんでした。あなたはトークンに権限を追加する必要があります。
あなたはどこに行けばいいでしょうか?正解は…
状況(というか作る物)にもよりますが,殆どの場合の正解はこれです。途中で出てきましたが, 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 とは別物だと考えています)
ややこしすぎるやろ!
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 returnmessage_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
の中に現れなくなっています。言葉よりも実際の状態を見てもらったほうがいいのでスクリーンショットを。
これは
- テキストボックスに内容を入力
- チェックボックスをチェック
- チェックボックスをチェックした瞬間の送信アクションを確認
という手順で調べたものですが,テキストボックスの中身を入れたはずなのになぜか null
になっています。これは Block Kit Builder の仕様で,実際はちゃんと値は入ってきます。この仕様を知らないと無駄に悩むことになると思うので,ここで覚えておいてください。
Block Kit で見た目が同じなのに分類や JSON ペイロードの持ち方が全く違うコントロールがある
辛い度: ★★★☆☆
Block Kit Builder の左側のペインを見てもらうと分かりますが,同じような名前のものが違うグルーピングのところに存在します。
これはどちらもラジオボタンですが,それぞれ
- 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 | 他を選択しない場合必須 | 必須 | 他を選択しない場合必須 | 必須 | 必須 |
メッセージ応答 | 任意 | ||||
モーダル状態コントロール | 任意 |
ここの「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"
}
{
"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
返信された瞬間にメッセージはスレッド扱いになり,未定義だったフィールドが後から生える
辛い度: ★★★★★
{
"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 の投稿に変身する
辛い度: ★★★★☆
お前は何を言っているんだ感がありますが,これです,これ。見たことありますよね。
これを 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 的なものです。この子です。
アイコン違うやん!ゴミ箱やん!
(てかサブタイプ tombstone
なのに墓石じゃないんや…)
パーマリンクを取得するためのエンドポイントがわざわざ存在する
辛い度: ★★★☆☆
また,リアクションに加えて パーマリンク も含まれている点も重要だったりします。その理由は後述します。
と書きましたが,その理由は パーマリンクが含まれないエンドポイントがある からです。
# チャンネルID/タイムスタンプ
https://.../archives/C02S0JR88AD/p1642505773002100
# チャンネルID/タイムスタンプ?thread_ts=スレッドルートのタイムスタンプ&cid=スレッドルートのチャンネルID
https://.../archives/C02S0JR88AD/p1642505773002100?thread_ts=1642504946.001400&cid=C02S0JR88AD
パーマリンクは上記のようなフォーマットとなりますが,ワークスペースの名前やスレッドの親子関係も含ませる必要があるため,自前生成はかなり骨が折れます。これは無いと困りますね。
パーマリンクを持たないレスポンスを返してくるエンドポイントの一例を挙げます。
-
conversations.history method | Slack
- チャンネルを指定して新しいほうからメッセージを取得するエンドポイント。返信は含まれない。
-
conversations.replies method | Slack
- スレッドを指定して新しいほうから返信を取得するエンドポイント。
-
reaction_added event | Slack
- リアクションが付いたことをリアルタイムで通知する Event API イベントのペイロード。
conversations.history |
conversations.replies |
reaction_added event |
|
---|---|---|---|
permalink を含む? |
|||
thread_ts を含む? |
✅ | ✅ |
これで最も困るのは, 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 の内部はかなり大変なことになっているようです。以前整理してみたときの樹形図があるので掲載します。
(一部私が考えた造語を使っています)
- Bot は SlackBot(公式の
USLACKBOT
) と CustomBot(ユーザが作るもの)の2種類がある - 通常ユーザは組織内しか見えないが, Slack Connect で繋がっている場合は外部でも見える
- 組織内ユーザは,通常ユーザとは別に招待できるゲストがあり, シングルチャンネルゲスト と マルチチャンネルゲスト の2種類がある
- 組織内ユーザのうち一部は強い権限を持ち,強い順に 代表オーナー, オーナー, 管理者 の3種類がある
いやーめちゃくちゃややこしいですね。そしてこれらを完全に判定・分離するロジックがこちら。
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
コマンド専用の謎のメッセージ形式がある
辛い度: ★★☆☆☆
ん?なんやこれ?
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
を使うべきと予想
- 多分
なお,名前以外にもまだ何個かダブり項目はあったような気がします…