slack について
Slack 連携をローカルでテストするための、状態を保持する Slack Web API エミュレーターです。メッセージ、会話、ファイル、OAuth、アプリ操作を扱い、シード状態や Webhook コールバックにも対応します。
主な機能
- 01状態を保持する Slack Web API エミュレーション
- 02メッセージ、スレッド、リアクション
- 03会話とユーザー状態の変更
- 04ファイルアップロード、ピン、ブックマーク
- 05OAuth とインタラクティブビュー
活用シーン
- 01実ワークスペースなしで Slack 連携をテスト
- 02メッセージやスレッド処理を検証
- 03OAuth とトークン処理を確認
- 04ファイル、ビュー、操作コールバックをテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill slackインストール後、Claude Code で/slackを入力して読み込まれたか確認します。
詳細
Slack API エミュレーター
チャンネル、メッセージ、スレッド、リアクション、ユーザープロフィール、プレゼンス、最新のファイルアップロード、ピン留め、ブックマーク、ビュー、OAuth v2、受信 Webhook を備えた、完全な状態管理に対応する Slack Web API のエミュレーションです。チャットへの書き込みでは、blocks、attachments、metadata、書式設定フラグ、展開制御フラグ、クライアントメッセージ ID など、一般的なリッチメッセージフィールドが保持されます。会話への書き込みでは、アーカイブ状態、名前、トピック、目的、メンバーシップ、DM、MPIM、既読カーソルが更新されます。ユーザーへの書き込みでは、プロフィールフィールド、ステータス、カスタムフィールド、決定的なアクティブまたは離席中のプレゼンスが更新されます。ファイルへの書き込みでは、ローカルのアップロード URL、ファイル共有メッセージ、読み込み、一覧表示、ダウンロード、削除に対応する現在の外部アップロードフローをサポートします。ピン留めとブックマークへの書き込みでは、チャンネルメッセージのピン留めとリンクのブックマークに対応します。ビューへの書き込みでは、App Home の公開とモーダルスタックに対応します。シードされた OAuth アプリと OAuth インストールにより、ボットユーザーとインストール記録が作成されます。OAuth の交換と明示的なトークンシードにより、スコープ付きトークン記録が作成されます。状態の変更に応じて、設定された Webhook URL に event_callback ペイロードが送信されます。
Slack のメッセージテキストは、チャットへの書き込み、受信 Webhook、ファイルアップロード時の初期コメントを通じて、Unicode 文字数で 40,000 文字に制限されます。これを超えるテキストは、保存または送信される前に、Unicode コードポイントの境界で切り詰められます。成功した Web API のレスポンスには、warning: "message_truncated" と、一致する警告および説明メッセージを含む response_metadata が含まれます。blocks や attachments などのリッチフィールドは変更されずに保持されます。
起動
ローカルリスナーは、デフォルトで 127.0.0.1 にバインドします。コンテナやほかのマシンからアクセスするには、CLI に --host 0.0.0.0 を渡すか、createEmulator に hostname: '0.0.0.0' を渡してください。クライアントからアクセスできる広告用 URL を設定するには、--base-url または baseUrl を指定してください。
# Slack only
npx emulate --service slack
# Default port (when run alone)
# http://localhost:4000
プログラムから起動する場合:
import { createEmulator } from 'emulate'
const slack = await createEmulator({ service: 'slack', port: 4003 })
// slack.url === 'http://localhost:4003'
認証
トークンは Authorization: Bearer <token> として渡してください。すべての Web API エンドポイントで認証が必要です。
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer test_token_admin"
トークンのないリクエストは not_authed を返します。スコープの緩和モードでは、空でない任意の未登録ベアラートークンが、最初にシードされたユーザーにマッピングされます。
ローカル開発では、デフォルトでスコープチェックが緩和されています。サポート対象の Web API メソッドで、Slack 形式の missing_scope エラーと needed および provided フィールドを返す必要がある場合は、シード設定で slack.strict_scopes: true を設定してください。厳格モードでは、chat:write、channels:read、channels:history、channels:join、channels:manage、channels:write、groups:read、groups:history、groups:write、im:read、im:history、im:write、mpim:read、mpim:history、mpim:write、users:read、users:read.email、users.profile:read、users.profile:write、users:write、files:read、files:write、pins:read、pins:write、bookmarks:read、bookmarks:write、reactions:read、reactions:write、team:read、admin.conversations:write をチェックします。Slack では views.publish、views.open、views.update、views.push にメソッド固有のスコープが示されていないため、エミュレーターではこれらのメソッドに認証を要求しますが、厳格なスコープチェックは追加しません。
アプリの接続先をエミュレーターに変更する
環境変数
SLACK_EMULATOR_URL=http://localhost:4003
Slack SDK/Bolt
import { WebClient } from '@slack/web-api'
const client = new WebClient(token, {
slackApiUrl: `${process.env.SLACK_EMULATOR_URL}/api/`,
})
OAuth URL のマッピング
| 実際の Slack URL | エミュレーター URL |
|---|---|
https://slack.com/oauth/v2/authorize | $SLACK_EMULATOR_URL/oauth/v2/authorize |
https://slack.com/api/oauth.v2.access | $SLACK_EMULATOR_URL/api/oauth.v2.access |
Auth.js/NextAuth.js
{
id: 'slack',
name: 'Slack',
type: 'oauth',
authorization: {
url: `${process.env.SLACK_EMULATOR_URL}/oauth/v2/authorize`,
params: { scope: 'chat:write,channels:read,users:read,users.profile:read' },
},
token: {
url: `${process.env.SLACK_EMULATOR_URL}/api/oauth.v2.access`,
},
clientId: process.env.SLACK_CLIENT_ID,
clientSecret: process.env.SLACK_CLIENT_SECRET,
}
Seed Config
slack:
team:
name: My Workspace
domain: my-workspace
users:
- name: developer
real_name: Developer
email: [email protected]
is_admin: true
profile:
title: Local Developer
status_text: Testing locally
status_emoji: ":computer:"
presence: active
- name: designer
real_name: Designer
email: [email protected]
profile:
title: Designer
presence: away
channels:
- name: general
topic: General discussion
- name: engineering
topic: Engineering discussions
is_private: true
bots:
- name: my-bot
oauth_apps:
- client_id: "12345.67890"
client_secret: example_client_secret
app_id: A000000001
name: My Slack App
redirect_uris:
- http://localhost:3000/api/auth/callback/slack
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
user_scopes:
- users:read
- users.profile:read
bot_name: my-bot
tokens:
- token: xoxb-local-test
user: developer
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
incoming_webhooks:
- channel: general
label: CI Notifications
strict_scopes: false
signing_secret: my_signing_secret
OAuth アプリが設定されていない場合、エミュレーターは任意の client_id を受け入れます。アプリが設定されている場合は、client_id、client_secret、redirect_uri の厳密な検証が行われます。
signing_secret は、送信されるすべてのイベントサブスクリプションコールバックに署名します。受信側のアプリにも同じシークレットを設定してください。署名付きコールバックには X-Slack-Request-Timestamp と X-Slack-Signature が含まれ、X-Slack-Signature は v0=<HMAC-SHA256(secret, "v0:<timestamp>:<raw-body>")> に設定されます。未解析のリクエスト本文を使って検証してください。シークレットがないか空の場合、コールバックには署名されません。
API エンドポイント
Auth
# Test authentication
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer $TOKEN"
Chat
chat.postMessage、chat.update、conversations.history、conversations.replies は、テキストと一般的なリッチメッセージフィールド(blocks、attachments、metadata、mrkdwn、parse、link_names、unfurl_links、unfurl_media、username、icon_url、icon_emoji、bot_id、app_id、client_msg_id、reply_broadcast)を送受信します。chat.postMessage は、開かれた DM の会話や、サポート対象の Slack ユーザー ID にも投稿できます。
chat.postEphemeral は、エフェメラルメッセージをチャンネル履歴の外部に保存します。chat.scheduleMessage、chat.deleteScheduledMessage、chat.scheduledMessages.list を使うと、スケジュール済みメッセージは削除または確認されるまで保留状態になります。
# Post message
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello from the emulator!"}'
# Post threaded reply
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Thread reply", "thread_ts": "1234567890.123456"}'
# Post rich message
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Deploy complete", "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": "*Deploy* complete"}}], "metadata": {"event_type": "deploy_complete", "event_payload": {"deploy_id": "dep_123"}}, "unfurl_links": false}'
# Update message
curl -X POST http://localhost:4003/api/chat.update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456", "text": "Updated message"}'
# Delete message
curl -X POST http://localhost:4003/api/chat.delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# Get message permalink
curl -X GET 'http://localhost:4003/api/chat.getPermalink?channel=C000000001&message_ts=1234567890.123456' \
-H "Authorization: Bearer $TOKEN"
# Post ephemeral message
curl -X POST http://localhost:4003/api/chat.postEphemeral \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "user": "U000000001", "text": "Only you can see this"}'
# Schedule message
curl -X POST http://localhost:4003/api/chat.scheduleMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"C000000001\", \"text\": \"Reminder\", \"post_at\": $(($(date +%s) + 3600))}"
# List scheduled messages
curl -X POST http://localhost:4003/api/chat.scheduledMessages.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Delete scheduled message
curl -X POST http://localhost:4003/api/chat.deleteScheduledMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "scheduled_message_id": "Q123456789"}'
# /me message
curl -X POST http://localhost:4003/api/chat.meMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "is thinking..."}'
Conversations
# List channels (cursor pagination)
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN"
# List DMs and MPIMs
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "im,mpim"}'
# Get channel info
curl -X POST http://localhost:4003/api/conversations.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Create channel
curl -X POST http://localhost:4003/api/conversations.create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "new-channel", "is_private": false}'
# Archive and unarchive a non-general channel
curl -X POST http://localhost:4003/api/conversations.archive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
curl -X POST http://localhost:4003/api/conversations.unarchive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
# Rename channel
curl -X POST http://localhost:4003/api/conversations.rename \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "name": "new-channel-name"}'
# Set topic / purpose
curl -X POST http://localhost:4003/api/conversations.setTopic \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "topic": "Release coordination"}'
curl -X POST http://localhost:4003/api/conversations.setPurpose \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "purpose": "Coordinate release work"}'
# Channel history (top-level messages only)
curl -X POST http://localhost:4003/api/conversations.history \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Thread replies
curl -X POST http://localhost:4003/api/conversations.replies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# Join / leave channel
curl -X POST http://localhost:4003/api/conversations.join \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Discover IDs before membership or DM examples
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "public_channel,private_channel"}'
CHANNEL_ID="<channel-id-from-conversations.list>"
USER_ID="<user-id-from-users.list>"
# Invite / kick user
curl -X POST http://localhost:4003/api/conversations.invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"users\": \"$USER_ID\"}"
curl -X POST http://localhost:4003/api/conversations.kick \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"user\": \"$USER_ID\"}"
# Open DM, then set DM_CHANNEL_ID to the returned channel.id
curl -X POST http://localhost:4003/api/conversations.open \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"users\": \"$USER_ID\", \"return_im\": true}"
DM_CHANNEL_ID="<channel.id-from-conversations.open>"
curl -X POST http://localhost:4003/api/conversations.close \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$DM_CHANNEL_ID\"}"
# Mark conversation read
curl -X POST http://localhost:4003/api/conversations.mark \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# List members
curl -X POST http://localhost:4003/api/conversations.members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
Users
# List users (cursor pagination)
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
# Get user info
curl -X POST http://localhost:4003/api/users.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001"}'
# Lookup by email
curl -X POST http://localhost:4003/api/users.lookupByEmail \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
# Get a user profile
curl -X GET 'http://localhost:4003/api/users.profile.get?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
# Update profile fields and status
curl -X POST http://localhost:4003/api/users.profile.set \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001", "profile": {"display_name": "Developer", "status_text": "Testing locally", "status_emoji": ":computer:"}}'
# Get presence
curl -X GET 'http://localhost:4003/api/users.getPresence?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
# Set the authed user away
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "away"}'
# Return the authed user to automatic presence
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "auto"}'
Files
# Request a local upload URL
curl -X POST http://localhost:4003/api/files.getUploadURLExternal \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filename": "deploy.txt", "length": 18}'
# Upload bytes to the returned upload_url
curl -X POST http://localhost:4003/upload/v1/F000000001 \
-H "Content-Type: application/octet-stream" \
--data-binary @deploy.txt
# Complete and share the file in a channel
curl -X POST http://localhost:4003/api/files.completeUploadExternal \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"files": [{"id": "F000000001", "title": "Deploy Log"}], "channel_id": "C000000001", "initial_comment": "Deploy log attached"}'
# Get file info
curl -X GET 'http://localhost:4003/api/files.info?file=F000000001' \
-H "Authorization: Bearer $TOKEN"
# List files
curl -X POST http://localhost:4003/api/files.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Download file bytes from url_private
curl -X GET http://localhost:4003/files-pri/F000000001/deploy.txt \
-H "Authorization: Bearer $TOKEN"
# Delete a file
curl -X POST http://localhost:4003/api/files.delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"file": "F000000001"}'
Pins And Bookmarks
# Pin a message
curl -X POST http://localhost:4003/api/pins.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
# List pinned messages
curl -X GET 'http://localhost:4003/api/pins.list?channel=C000000001' \
-H "Authorization: Bearer $TOKEN"
# Remove a message pin
curl -X POST http://localhost:4003/api/pins.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
# Add a link bookmark
curl -X POST http://localhost:4003/api/bookmarks.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "title": "Runbook", "type": "link", "link": "https://example.com/runbook"}'
# Edit a bookmark
curl -X POST http://localhost:4003/api/bookmarks.edit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "bookmark_id": "Bk000000001", "title": "Updated Runbook"}'
# List bookmarks
curl -X POST http://localhost:4003/api/bookmarks.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001"}'
# Remove a bookmark
curl -X POST http://localhost:4003/api/bookmarks.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "bookmark_id": "Bk000000001"}'
Views
# Publish an App Home view
curl -X POST http://localhost:4003/api/views.publish \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "U000000001", "view": {"type": "home", "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Local App Home"}}]}}'
# Generate a local trigger id for modal tests
curl -X POST http://localhost:4003/api/views.generateTriggerId \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "U000000001"}'
# Open a modal view with the returned trigger_id or interactivity_pointer before it expires
curl -X POST http://localhost:4003/api/views.open \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_id": "<trigger_id from views.generateTriggerId>", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Local Modal"}, "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Modal body"}}]}}'
# Update a view
curl -X POST http://localhost:4003/api/views.update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"view_id": "V000000001", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Updated Modal"}, "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Updated body"}}]}}'
# Generate a push trigger tied to an existing modal view
curl -X POST http://localhost:4003/api/views.generateTriggerId \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"view_id": "V000000001"}'
# Push a modal view onto the current stack with the returned trigger_id or interactivity_pointer
curl -X POST http://localhost:4003/api/views.push \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_id": "<trigger_id from views.generateTriggerId>", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Next Modal"}, "blocks": []}}'
Reactions
# Add reaction
curl -X POST http://localhost:4003/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
# Remove reaction
curl -X POST http://localhost:4003/api/reactions.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
# Get reactions
curl -X POST http://localhost:4003/api/reactions.get \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
Team
# Get workspace info
curl -X POST http://localhost:4003/api/team.info \
-H "Authorization: Bearer $TOKEN"
Bots
# Get bot info
curl -X POST http://localhost:4003/api/bots.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bot": "B000000001"}'
Incoming Webhooks
# Post via incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Deployment complete!"}'
# Post to a specific channel
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Alert!", "channel": "C000000002"}'
# Post threaded webhook message
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Thread update", "thread_ts": "1234567890.123456"}'
Interactivity
ボタンやモーダルの送信によって、アプリの Interactivity Request URL を呼び出せます。Seed された slack.oauth_apps エントリに interactivity_url を設定し、slack.signing_secret に受信側と同じシークレットを設定してください。
slack:
signing_secret: local-signing-secret
oauth_apps:
- app_id: A_MY_APP
client_id: my-client
client_secret: my-secret
name: My App
redirect_uris: []
interactivity_url: http://localhost:3000/slack/interactions
POST /_slack/simulate/block-actionsは、channel、message_ts、ボタンのセレクター(action_id、完全一致するtext、またはurl)、および操作するユーザーの ID または名前を受け取ります。アクションブロックやセクションのアクセサリーにあるボタンに対応し、そのユーザーに表示されるエフェメラルメッセージ内のボタンも対象です。シミュレーターには認証済みのエミュレータートークンが必要で、操作するユーザーは会話に参加している必要があります。POST /_slack/simulate/view-submissionは、view_id、任意のuser、および入力ブロック ID をキーとするvaluesを受け取ります。テキスト入力には文字列、静的セレクトとラジオボタンにはオプション値、複数選択とチェックボックスにはオプション値の配列を指定します。省略された値には、以前の入力値または初期値が保持されます。その他の入力要素はunsupported_elementを返します。
コールバックは application/x-www-form-urlencoded 形式で、payload という JSON フィールドを1つ含みます。署名はエンコード済みのリクエスト本文そのものに対して行われます。ボタンのペイロードには、アプリに紐付けられた一度だけ使える trigger_id が含まれ、操作するユーザーのモーダルを開くことができます。トリガーはデフォルトで3秒後に期限切れになります。ローカルテストで時間を延ばすには、slack.view_trigger_ttl_seconds に正の数を設定してください。アプリのボットトークンまたはユーザートークンを使って投稿されたメッセージでは、アプリの所有権が保持されます。シミュレーターはペイロードと delivery_id を返し、試行と配信結果は slack.interaction_deliveries に保存されます。
ボタンのペイロードに含まれる response_url は、ベアラートークンなしで最大5回、30分以内に JSON を受け付けます。デフォルトではエフェメラルな返信を投稿し、response_type: "in_channel" を指定すると公開返信を投稿します。元のメッセージを変更するには replace_original: true、削除するには delete_original: true を指定します。置換後も元のメッセージの可視性は保持されます。リッチブロックとスレッドへの返信に対応しています。
モーダルの受信側は3秒以内に応答する必要があります。空の HTTP 200 は送信されたビューを閉じ、response_action: "clear" はビューのスタックを閉じます。errors はビューを保持してブロックエラーを返し、update は内容を置き換え、push は3ビューまでという上限内で新しいビューを追加します。配信に失敗した場合や無効な応答を受け取った場合、モーダルは開いたままになります。ショートカット、スラッシュコマンド、ボタン以外のアクション、view_closed、block_suggestion はシミュレーションされません。
OAuth
# Authorize (browser flow, shows user picker)
# GET /oauth/v2/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# The picker submits POST /oauth/v2/authorize/callback to create the auth code.
# Token exchange
curl -X POST http://localhost:4003/api/oauth.v2.access \
-H "Content-Type: application/json" \
-d '{"client_id": "12345.67890", "client_secret": "example_client_secret", "code": "<code>"}'
Slack 形式のレスポンスを返します。
{
"ok": true,
"access_token": "xoxb-...",
"token_type": "bot",
"scope": "chat:write,channels:read",
"app_id": "A000000001",
"bot_user_id": "U000000099",
"team": { "id": "T000000001", "name": "Emulate" },
"authed_user": { "id": "U000000001" }
}
POST /api/apps.uninstall は、client_id、client_secret、およびインストール時のユーザートークンを、Authorization ヘッダーまたは POST の token フィールドで受け取ります。トークンのワークスペースから該当アプリのインストールを削除し、以前の許可分も含め、関連するすべての OAuth ボットトークンとユーザートークンを無効化します。無効化された認証情報を使った Web API 呼び出しは、認証が許容される設定でも token_revoked を返します。他のアプリやワークスペースの認証情報には影響しません。きめ細かなボットトークンではアプリをアンインストールできず、no_permission が返されます。アプリとの紐付けがない開発用トークンを設定している場合は、ローカルインストールをアンインストールできます。メソッドスコープは不要です。
アンインストール時には、削除されたアプリの api_app_id に一致するイベントサブスクリプションに app_uninstalled と tokens_revoked が送信されます。tokens_revoked.tokens.oauth と .bot には重複を除いたユーザー ID が含まれ、トークン文字列は含まれません。コールバックが配信される前に認証情報は無効になります。ハンドラーは、どちらのイベントが先に届いても処理できるようにしてください。新たな OAuth インストールに備えてアプリ定義は保持されます。再インストールすると新しいトークンが発行されますが、無効化されたトークンが再び有効になることはありません。
インスペクター
Slack エミュレーターで GET / を開くと、会話、メッセージ、ファイル、ビュー、認証レコード、受信 Webhook、イベントサブスクリプション、イベント配信を確認できます。
イベントのディスパッチ
イベントを発生させる対応済みの Slack 書き込みでは、エミュレーターが設定済みの Webhook URL に event_callback ペイロードをディスパッチします。これらのペイロードは Slack の Events API 形式に準拠しています。
各コールバックには、内側の event に加え、外側の token、api_app_id、authorizations、team_id、event_id、整数の Unix 秒単位の event_time が含まれます。認証済みの Web API 書き込みでは、team_id は提示された Slack トークンのインストール情報から取得されます。保存済みの Slack レコードがない開発用トークンの場合は、対象のチャンネル、ユーザー、またはファイルのチームを使用し、続いてシード済みワークスペースのチーム(または T000000001)を使用します。受信 Webhook の投稿では、Webhook レコードのチームを使用します。一致するレコードがない場合は、対象チャンネルのチームを使用します。各論理イベントには個別の event_id が割り当てられ、複数のサブスクライバーへの配信で共通して使用されます。
コールバックは、各ワークスペースのインストールを、受信先の api_app_id とボット認証情報で識別します。複数のアプリがインストールされている場合、各アプリには個別のエンベロープが送信されます。設定済みの Webhook サブスクリプションは共有され、api_app_id による振り分けが可能です。インストールがない場合、コールバックでは A000000001 とローカルユーザーの認証情報が使用されます。従来の token フィールドはデフォルトで emulate-verification-token になります。この値を確認する受信側に合わせるには、slack.verification_token を設定してください。リクエストの署名検証には slack.signing_secret を使用してください。
すべての内側のイベントには event_ts が含まれます。メッセージイベントには channel_type(channel、group、im、または mpim)が含まれます。発行済みまたはシード済みのボットトークンで投稿した場合、対応するボットレコードが存在すれば、メッセージにはボットの bot_id、app_id、bot_profile が含まれます。これらのフィールドは履歴、返信、更新にも保持されるため、ハンドラーは自身のメッセージを識別できます。conversations.kick または conversations.leave でインストール済みボットを削除すると、対象アプリに対して actor_id と event_ts を含む channel_left または group_left も送信されます。
ボットのハンドラーをテストするには、インストール済みアプリの bot_user_id を conversations.invite で招待し、app_mention をサブスクライブしてから、chat.postMessage で <@BOT_USER_ID> を含むテキストを投稿します。チャンネルでのメンションでは、メンションされたアプリごとにコールバックが 1 件送信されます。外側の api_app_id、内側の event_ts、元のメッセージとスレッドの識別情報が含まれます。DM と MPIM では message イベントが使用されます。
chat.postMessageでのmessageイベント- チャンネルメッセージのテキストでインストール済みのメンバーボットがメンションされた場合の、
chat.postMessageでのapp_mention apps.uninstallの成功時のapp_uninstalled/tokens_revokedchat.updateでのsubtype: message_changedを含むmessagechat.deleteでのsubtype: message_deletedを含むmessage- 投稿された
messageイベントには、存在する場合、リッチメッセージのフィールドが含まれます reactions.add/reactions.removeでのreaction_added/reaction_removedイベントpins.add/pins.removeでのpin_added/pin_removedイベント- インカミングウェブフックでの投稿時に、
subtype: bot_messageを含むmessage - 公開チャンネルのライフサイクル上のアーカイブ操作での
channel_archive/channel_unarchive - プライベートチャンネルのライフサイクル上のアーカイブ操作での
group_archive/group_unarchive conversations.renameでのchannel_rename/group_renameと、対応する名前変更のメッセージサブタイプ- トピックおよび目的の変更操作での、公開チャンネル向け
channel_topic/channel_purposeまたはプライベートチャンネル向けgroup_topic/group_purposeのサブタイプを含むmessage - 招待、参加、退出、キックの操作での
member_joined_channel/member_left_channel - インストール済みのボットが退出または削除された場合の
channel_left/group_left - DM および MPIM の操作での
im_created、im_open、im_close、im_markedと、グループの開く/閉じる/既読にするイベント - プロフィールの変更操作での
user_change - プレゼンスの変更操作での
presence_change - ファイルの操作での
file_created、file_shared、file_deleted - 共有ファイルのアップロード時に、
subtype: file_shareを含むmessage
slack.signing_secret が設定されている場合、既存のすべての送信イベントサブスクリプションコールバックに X-Slack-Request-Timestamp と X-Slack-Signature が含まれます。署名は v0=<HMAC-SHA256(secret, "v0:<timestamp>:<raw-body>")> で、正確にシリアライズされたコールバック本文を使用します。受信側には同じシークレットを設定し、解析前のリクエスト本文を検証してください。シークレットが未設定または空の場合、コールバックには署名が付与されません。
チャンネルのプライバシー変換
POST /api/admin.conversations.convertToPrivate と POST /api/admin.conversations.convertToPublic は channel_id を受け付けます。シード済みの管理者に属するユーザートークンを使用してください。ボットトークンとテストトークンでは not_allowed_token_type が返され、管理者以外のユーザーでは missing_scope が返されます。slack.strict_scopes: true の場合、トークンには admin.conversations:write も必要です。例:
slack:
tokens:
- token: local-admin-user
type: user
user: admin
scopes: [admin.conversations:write, channels:read, groups:read]
変換後もチャンネル ID、メンバー、履歴は保持され、会話とファイルの読み取りでの公開範囲が更新されます。また、channel_convert_to_private または channel_convert_to_public をサブタイプとする message が追加されます。イベントコールバックには、操作を行ったユーザーと新しい channel_type(group または channel)が含まれます。管理者はチャンネルに所属している必要はありません。DM、MPIM、および繰り返しの変換は拒否されます。#general をプライベートに変更することはできません。
これらのメソッドは、Slack の Enterprise プランや組織全体へのインストール要件なしで、エミュレートされたワークスペースを操作します。Enterprise のロール、チャンネル管理ポリシー、MPIM の変換はシミュレートされません。
Slack Connect のフィクスチャ
異なる team_id を持つユーザーをシードして、別のワークスペースからのメッセージをテストできます。users.info と users.list は、そのユーザーのワークスペースと is_stranger フラグを返します。ユーザーのチームを省略すると、エミュレートされたワークスペースがデフォルトになります。is_stranger のデフォルトは false です。アプリから見える共有チャンネルに参加していないユーザーを再現する場合は、シード時に設定できます。
slack:
users:
- name: partner
team_id: T_PARTNER
is_stranger: false
tokens:
- token: xoxp-partner
type: user
user: partner
scopes: [chat:write]
シードしたユーザーのトークンを使って、外部ユーザーとして操作します。conversations.invite でユーザーとアプリのボットをチャンネルに招待し、ボットにメンションするメッセージを投稿します。app-mention イベントには投稿者の user_team と source_team が含まれます。外側の team_id は、トークンで選択された、メッセージを受け取るインストール先のワークスペースを示します。デフォルトでは、エミュレートされたワークスペースです。通常のメッセージコールバックには投稿者のユーザー ID が保持されており、users.info で解決できます。
外部メンバーがいるチャンネルは、is_ext_shared: true と is_shared: true を返します。そのイベントコールバックには is_ext_shared_channel: true が含まれます。すでに共有済みのチャンネルを再現するには、channels[].is_ext_shared: true をシードします。最後の外部メンバーが退出または削除された後も、保存状態をまたいで共有状態は維持されます。Slack Connect の招待および承認フロー、ワークスペースの切断、ワークスペースごとのチャンネル設定はシミュレートされません。
イベントの再試行
認証済みのエミュレータートークンを使い、JSON { "event_id": "Ev...", "reason": "http_timeout" } を指定して POST /_slack/simulate/event-retry を実行すると、イベントの重複排除をテストできます。受信したコールバック、またはインスペクターの Events タブからイベント ID を取得してください。シミュレーターは、そのイベントの最新の記録済み配信を元のサブスクリプションに再送します。ペイロード全体とイベント ID は保持されます。複数のサブスクライバーが受信している場合は、最新の受信者だけが再試行されます。
各試行の X-Slack-Retry-Num は 1 から始まり、試行ごとに増加します。また、X-Slack-Retry-Reason も設定されます(デフォルトは http_timeout)。理由には http_error、connection_failed、ssl_error、too_many_redirects、unknown_error も指定できます。slack.signing_secret が設定されている場合、再試行では新しいリクエストタイムスタンプと署名が使用されます。レスポンスには delivery_id、retry_num、success、status_code が含まれます。コールバックに失敗した場合も記録され、再試行番号は消費されます。
再試行は明示的に実行するもので、自動スケジュールや3回までの制限はありません。最新のイベント配信1,000件のみがメモリに保持されるため、履歴が消去された後やエミュレーターの再起動後は再試行できません。不明なイベントには event_not_found が返され、サブスクリプションが非アクティブの場合は subscription_inactive が返されます。
現在の制限
Slack Connect の招待および承認フロー、ワークスペースごとのチャンネル設定、その他の Enterprise Grid 管理 API、Audit Logs API、SCIM、Legal Holds、Socket Mode、スラッシュコマンド、ショートカット、ボタンアクションとモーダル送信以外のインタラクションタイプ、ユーザーグループ、リマインダー、スター、通話、キャンバス、リスト、関数、ワークフロー、チャットストリーミング、旧式の files.upload、正確なレート制限、有料プランの動作は実装されていません。
よくあるパターン
メッセージの投稿とリアクション
TOKEN="test_token_admin"
BASE="http://localhost:4003"
# Post a message
curl -X POST $BASE/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello!"}'
# React to it (use the ts from the response)
curl -X POST $BASE/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "<ts>", "name": "wave"}'
OAuth フロー
- ユーザーを
$SLACK_EMULATOR_URL/oauth/v2/authorize?client_id=...&redirect_uri=...&scope=chat:write,channels:read&state=...にリダイレクトします。 - ユーザーがエミュレーターの UI でシード済みのユーザーを選択します。
- エミュレーターが
?code=...&state=...を付けて戻り先にリダイレクトします。 POST /api/oauth.v2.accessでコードをトークンに交換します。- Web API エンドポイントの呼び出しには
xoxb-トークンを使用します。
認可 URL とコールバックに user_scope が含まれている場合、交換レスポンスには authed_user.access_token、authed_user.scope、authed_user.token_type が含まれます。
Webhook による CI 通知
# Use the default incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Build passed on main"}'
組み込みエミュレーターとカスタムエミュレーターの併用
npx emulate init --custom inventory を使用して、サードパーティ API エミュレーターをスキャフォールディングし、テストします。組み込みの { emulator: "slack" } などのエントリーとともに、emulate の defineConfig を使用して emulate.config.ts に登録します。npx emulate start --watch を実行すると、インポートが再読み込みされ、出力された /_emulate URL でカスタム状態を確認できます。再読み込みに成功すると、実行状態はシードにリセットされます。既存のフラットなシード設定も引き続き動作します。明示的なファイルを指定するには --config を使用します。サードパーティ API エミュレーターの作成とテストについては、https://emulate.dev/docs/custom-emulators を参照してください。