Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 50/100

slack-bot-builder

PythonやJavaScript、JavaでBolt frameworkを使ったSlackアプリを構築します。リッチなUIを実現するBlock Kit、インタラクティブコンポーネント、スラッシュコマンド、イベントハンドリング、OAuthインストールフロー、Workflow Builder連携まで幅広くカバーします。

description の原文を見る

Build Slack apps using the Bolt framework across Python, JavaScript, and Java. Covers Block Kit for rich UIs, interactive components, slash commands, event handling, OAuth installation flows, and Workflow Builder integration.

SKILL.md 本文

Slack Bot Builder

Bolt フレームワークを使用して、Python、JavaScript、Java 全体で Slack アプリを構築します。 リッチ UI 用の Block Kit、インタラクティブコンポーネント、スラッシュコマンド、 イベント処理、OAuth インストールフロー、および Workflow Builder 統合をカバーします。 本番対応 Slack アプリのベストプラクティスに焦点を当てます。

パターン

Bolt App Foundation パターン

Bolt フレームワークは、Slack アプリ構築のための Slack 推奨アプローチです。 認証、イベントルーティング、リクエスト検証、HTTP リクエスト処理を処理するため、 アプリロジックに焦点を当てることができます。

主な利点:

  • わずか数行のコードでのイベント処理
  • セキュリティチェックとペイロード検証が組み込み
  • 整理されたコンシステントなパターン
  • 実験と本番で動作

利用可能: Python、JavaScript (Node.js)、Java

使用時期: 新しい Slack アプリを開始する場合、レガシー Slack API から移行する場合、本番 Slack 統合を構築する場合

Python Bolt App

from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import os

環境からトークンを初期化

app = App( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"] )

"hello" を含むメッセージを処理

@app.message("hello") def handle_hello(message, say): """'hello' を含むメッセージに応答します。""" user = message["user"] say(f"Hey there <@{user}>!")

スラッシュコマンドを処理

@app.command("/ticket") def handle_ticket_command(ack, body, client): """/ticket スラッシュコマンドを処理します。""" # 即座に確認(3秒以内) ack()

# チケット作成用のモーダルを開く
client.views_open(
    trigger_id=body["trigger_id"],
    view={
        "type": "modal",
        "callback_id": "ticket_modal",
        "title": {"type": "plain_text", "text": "Create Ticket"},
        "submit": {"type": "plain_text", "text": "Submit"},
        "blocks": [
            {
                "type": "input",
                "block_id": "title_block",
                "element": {
                    "type": "plain_text_input",
                    "action_id": "title_input"
                },
                "label": {"type": "plain_text", "text": "Title"}
            },
            {
                "type": "input",
                "block_id": "desc_block",
                "element": {
                    "type": "plain_text_input",
                    "multiline": True,
                    "action_id": "desc_input"
                },
                "label": {"type": "plain_text", "text": "Description"}
            },
            {
                "type": "input",
                "block_id": "priority_block",
                "element": {
                    "type": "static_select",
                    "action_id": "priority_select",
                    "options": [
                        {"text": {"type": "plain_text", "text": "Low"}, "value": "low"},
                        {"text": {"type": "plain_text", "text": "Medium"}, "value": "medium"},
                        {"text": {"type": "plain_text", "text": "High"}, "value": "high"}
                    ]
                },
                "label": {"type": "plain_text", "text": "Priority"}
            }
        ]
    }
)

モーダル送信を処理

@app.view("ticket_modal") def handle_ticket_submission(ack, body, client, view): """チケットモーダル送信を処理します。""" ack()

# ビューから値を抽出
values = view["state"]["values"]
title = values["title_block"]["title_input"]["value"]
desc = values["desc_block"]["desc_input"]["value"]
priority = values["priority_block"]["priority_select"]["selected_option"]["value"]
user_id = body["user"]["id"]

# システム内でチケットを作成
ticket_id = create_ticket(title, desc, priority, user_id)

# ユーザーに通知
client.chat_postMessage(
    channel=user_id,
    text=f"Ticket #{ticket_id} created: {title}"
)

ボタンクリックを処理

@app.action("approve_button") def handle_approval(ack, body, client): """承認ボタンクリックを処理します。""" ack()

# アクションからコンテキストを取得
user = body["user"]["id"]
action_value = body["actions"][0]["value"]

# メッセージを更新してインタラクティブ要素を削除
# (ベストプラクティス:ダブルクリックを防止)
client.chat_update(
    channel=body["channel"]["id"],
    ts=body["message"]["ts"],
    text=f"Approved by <@{user}>",
    blocks=[]  # インタラクティブブロックを削除
)

app_home_opened イベントをリッスン

@app.event("app_home_opened") def update_home_tab(client, event): """ユーザーが Home タブを開いたときに更新します。""" client.views_publish( user_id=event["user"], view={ "type": "home", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Welcome to the Ticket Bot!" } }, { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": "Create Ticket"}, "action_id": "create_ticket_button" } ] } ] } )

開発用 Socket Mode(公開 URL 不要)

if name == "main": handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]) handler.start()

本番環境では、HTTP モードと web サーバーを使用

from flask import Flask, request

from slack_bolt.adapter.flask import SlackRequestHandler

flask_app = Flask(name)

handler = SlackRequestHandler(app)

@flask_app.route("/slack/events", methods=["POST"])

def slack_events():

return handler.handle(request)

アンチパターン

  • 3 秒以内にリクエストを確認しない
  • ack ハンドラで処理がブロックされている
  • ソースコードにトークンがハードコードされている
  • 開発に Socket Mode を使用していない

Block Kit UI パターン

Block Kit は、リッチでインタラクティブなメッセージを構築するための Slack の UI フレームワークです。 ブロック(セクション、アクション、入力)と要素(ボタン、メニュー、テキスト入力)を使用してメッセージを作成します。

制限:

  • メッセージあたり最大 50 ブロック
  • モーダル/Home タブで最大 100 ブロック
  • ブロックテキストは 3000 文字に制限

Block Kit Builder を使用してプロトタイプを作成:https://app.slack.com/block-kit-builder

使用時期: リッチなメッセージレイアウトを構築する場合、メッセージにインタラクティブコンポーネントを追加する場合、モーダルでフォームを作成する場合、Home タブ体験を構築する場合

from slack_bolt import App import os

app = App(token=os.environ["SLACK_BOT_TOKEN"])

def build_notification_blocks(incident: dict) -> list: """インシデント通知用の Block Kit ブロックを構築します。""" severity_emoji = { "critical": ":red_circle:", "high": ":large_orange_circle:", "medium": ":large_yellow_circle:", "low": ":white_circle:" }

return [
    # ヘッダー
    {
        "type": "header",
        "text": {
            "type": "plain_text",
            "text": f"{severity_emoji.get(incident['severity'], '')} Incident Alert"
        }
    },
    # 詳細セクション
    {
        "type": "section",
        "fields": [
            {
                "type": "mrkdwn",
                "text": f"*Incident:*\n{incident['title']}"
            },
            {
                "type": "mrkdwn",
                "text": f"*Severity:*\n{incident['severity'].upper()}"
            },
            {
                "type": "mrkdwn",
                "text": f"*Service:*\n{incident['service']}"
            },
            {
                "type": "mrkdwn",
                "text": f"*Reported:*\n<!date^{incident['timestamp']}^{date_short} {time}|{incident['timestamp']}>"
            }
        ]
    },
    # 説明
    {
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f"*Description:*\n{incident['description'][:2000]}"
        }
    },
    # 区切り線
    {"type": "divider"},
    # アクションボタン
    {
        "type": "actions",
        "block_id": f"incident_actions_{incident['id']}",
        "elements": [
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "Acknowledge"},
                "style": "primary",
                "action_id": "acknowledge_incident",
                "value": incident['id']
            },
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "Resolve"},
                "style": "danger",
                "action_id": "resolve_incident",
                "value": incident['id'],
                "confirm": {
                    "title": {"type": "plain_text", "text": "Resolve Incident?"},
                    "text": {"type": "mrkdwn", "text": "Are you sure this incident is resolved?"},
                    "confirm": {"type": "plain_text", "text": "Yes, Resolve"},
                    "deny": {"type": "plain_text", "text": "Cancel"}
                }
            },
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "View Details"},
                "action_id": "view_incident",
                "value": incident['id'],
                "url": f"https://incidents.example.com/{incident['id']}"
            }
        ]
    },
    # コンテキストフッター
    {
        "type": "context",
        "elements": [
            {
                "type": "mrkdwn",
                "text": f"Incident ID: {incident['id']} | <https://runbook.example.com/{incident['service']}|View Runbook>"
            }
        ]
    }
]

def send_incident_notification(channel: str, incident: dict): """Block Kit を使用してインシデント通知を送信します。""" blocks = build_notification_blocks(incident)

app.client.chat_postMessage(
    channel=channel,
    text=f"Incident: {incident['title']}",  # 通知のフォールバック
    blocks=blocks
)

ボタンアクションを処理

@app.action("acknowledge_incident") def handle_acknowledge(ack, body, client): """インシデント確認を処理します。""" ack()

incident_id = body["actions"][0]["value"]
user = body["user"]["id"]

# システムを更新
acknowledge_incident(incident_id, user)

# メッセージを更新して確認を表示
original_blocks = body["message"]["blocks"]

# コンテキストに確認を追加
original_blocks[-1]["elements"].append({
    "type": "mrkdwn",
    "text": f":white_check_mark: Acknowledged by <@{user}>"
})

# 確認ボタンを削除(ダブルクリックを防止)
action_block = next(b for b in original_blocks if b.get("block_id", "").startswith("incident_actions"))
action_block["elements"] = [e for e in action_block["elements"] if e["action_id"] != "acknowledge_incident"]

client.chat_update(
    channel=body["channel"]["id"],
    ts=body["message"]["ts"],
    blocks=original_blocks
)

インタラクティブな選択メニュー

def build_user_selector_blocks(): """ユーザーセレクターを含むブロックを構築します。""" return [ { "type": "section", "text": {"type": "mrkdwn", "text": "Assign this task:"}, "accessory": { "type": "users_select", "action_id": "assign_user", "placeholder": {"type": "plain_text", "text": "Select assignee"} } } ]

オーバーフローメニューでオプションをさらに

def build_task_blocks(task: dict): """オーバーフローメニュー付きのタスクブロックを構築します。""" return [ { "type": "section", "text": {"type": "mrkdwn", "text": f"{task['title']}"}, "accessory": { "type": "overflow", "action_id": "task_overflow", "options": [ { "text": {"type": "plain_text", "text": "Edit"}, "value": f"edit_{task['id']}" }, { "text": {"type": "plain_text", "text": "Delete"}, "value": f"delete_{task['id']}" }, { "text": {"type": "plain_text", "text": "Share"}, "value": f"share_{task['id']}" } ] } } ]

アンチパターン

  • メッセージあたり 50 ブロックを超える
  • アクセシビリティのためのフォールバックテキストを提供しない
  • action_id をハードコードする(必要に応じて動的 ID を使用)
  • ボタンクリックをべき等で処理しない

OAuth インストールパターン

ユーザーが OAuth 2.0 経由でアプリをワークスペースにインストールできるようにします。 Bolt は OAuth フロー の大部分を処理しますが、設定してトークンを安全に保存する必要があります。

主な OAuth の概念:

  • スコープは権限を定義します(必要な最小限をリクエスト)
  • トークンはワークスペース固有です
  • インストールデータは永続的に保存する必要があります
  • ユーザーは後でスコープを追加できます(加算的)

ユーザーの 70% は、過度な権限リクエストに直面するとインストールを中止します。 必要なもののみをリクエストしてください!

使用時期: 複数のワークスペースにアプリを配布する場合、公開 Slack アプリを構築する場合、エンタープライズ級の統合

from slack_bolt import App from slack_bolt.oauth.oauth_settings import OAuthSettings from slack_sdk.oauth.installation_store import FileInstallationStore from slack_sdk.oauth.state_store import FileOAuthStateStore import os

本番環境では、データベース支援ストアを使用

例:PostgreSQL、MongoDB、Redis

class DatabaseInstallationStore: """インストールデータをデータベースに保存します。"""

async def save(self, installation):
    """ユーザーが OAuth を完了したときにインストールを保存します。"""
    await db.installations.upsert({
        "team_id": installation.team_id,
        "enterprise_id": installation.enterprise_id,
        "bot_token": encrypt(installation.bot_token),
        "bot_user_id": installation.bot_user_id,
        "bot_scopes": installation.bot_scopes,
        "user_id": installation.user_id,
        "installed_at": installation.installed_at
    })

async def find_installation(self, *, enterprise_id, team_id, user_id=None, is_enterprise_install=False):
    """ワークスペースのインストールを検索します。"""
    record = await db.installations.find_one({
        "team_id": team_id,
        "enterprise_id": enterprise_id
    })

    if record:
        return Installation(
            bot_token=decrypt(record["bot_token"]),
            # ... その他のフィールド
        )
    return None

OAuth 対応アプリを初期化

app = App( signing_secret=os.environ["SLACK_SIGNING_SECRET"], oauth_settings=OAuthSettings( client_id=os.environ["SLACK_CLIENT_ID"], client_secret=os.environ["SLACK_CLIENT_SECRET"], scopes=[ "channels:history", "channels:read", "chat:write", "commands", "users:read" ], user_scopes=[], # 必要に応じてユーザートークンスコープ installation_store=DatabaseInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=600) ) )

OAuth ルートは Bolt によって自動的に処理されます

/slack/install - OAuth フローを開始

/slack/oauth_redirect - コールバックを処理

Flask 統合

from flask import Flask, request from slack_bolt.adapter.flask import SlackRequestHandler

flask_app = Flask(name) handler = SlackRequestHandler(app)

@flask_app.route("/slack/install", methods=["GET"]) def install(): return handler.handle(request)

@flask_app.route("/slack/oauth_redirect", methods=["GET"]) def oauth_redirect(): return handler.handle(request)

@flask_app.route("/slack/events", methods=["POST"]) def slack_events(): return handler.handle(request)

インストール成功/失敗を処理

@app.oauth_success def handle_oauth_success(args): """OAuth が正常に完了したときに呼び出されます。""" installation = args["installation"]

# ウェルカムメッセージを送信
app.client.chat_postMessage(
    token=installation.bot_token,
    channel=installation.user_id,
    text="Thanks for installing! Type /help to get started."
)

return "Installation successful! You can close this window."

@app.oauth_failure def handle_oauth_failure(args): """OAuth が失敗したときに呼び出されます。""" error = args.get("error", "Unknown error") return f"Installation failed: {error}"

スコープ管理 - 必要に応じて追加スコープをリクエスト

def request_additional_scopes(team_id: str, new_scopes: list): """ ユーザーがスコープを追加するための URL を生成します。 注:既存のトークンは古いスコープを保持します。 新しいスコープの場合、ユーザーは再度認可する必要があります。 """ base_url = "https://slack.com/oauth/v2/authorize" params = { "client_id": os.environ["SLACK_CLIENT_ID"], "scope": ",".join(new_scopes), "team": team_id } return f"{base_url}?{urlencode(params)}"

アンチパターン

  • 不必要なスコープを事前にリクエストする
  • トークンをプレーンテキストで保存する
  • OAuth state パラメータを検証しない(CSRF リスク)
  • 設定変更後、新しいスコープでトークンを持つと仮定する

Socket Mode パターン

Socket Mode を使用すると、アプリは公開 HTTP エンドポイントの代わりに WebSocket 経由でイベントを受け取ることができます。 開発およびファイアウォールの背後にあるアプリに最適です。

利点:

  • 公開 URL は不要
  • 企業ファイアウォールの背後で動作
  • よりシンプルなローカル開発
  • リアルタイム双方向通信

制限:高トラフィック本番アプリには推奨されません。

使用時期: ローカル開発、企業ファイアウォールの背後にあるアプリ、セキュリティ制約のある内部ツール、プロトタイプとテスト

from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import os

Socket Mode はアプリレベルトークン(xapp-...)が必要です

アプリ設定 > 基本情報 > アプリレベルトークンで作成

'connections:write' スコープが必要

app = App(token=os.environ["SLACK_BOT_TOKEN"])

@app.message("hello") def handle_hello(message, say): say(f"Hey <@{message['user']}>!")

@app.command("/status") def handle_status(ack, say): ack() say("All systems operational!")

@app.event("app_mention") def handle_mention(event, say): say(f"You mentioned me, <@{event['user']}>!")

if name == "main": # SocketModeHandler は WebSocket 接続を管理します handler = SocketModeHandler( app, os.environ["SLACK_APP_TOKEN"] # xapp-... トークン )

print("Starting Socket Mode...")
handler.start()

非同期アプリの場合

from slack_bolt.async_app import AsyncApp from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler import asyncio

async_app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"])

@async_app.message("hello") async def handle_hello_async(message, say): await say(f"Hey <@{message['user']}>!")

async def main(): handler = AsyncSocketModeHandler(async_app, os.environ["SLACK_APP_TOKEN"]) await handler.start_async()

if name == "main": asyncio.run(main())

アンチパターン

  • 高トラフィック本番アプリに Socket Mode を使用する
  • WebSocket 接続切断を処理しない
  • アプリレベルトークンの作成を忘れる
  • ボットトークンの代わりにアプリトークンを使用する

Workflow Builder ステップパターン

Slack の Workflow Builder を、アプリによる カスタムステップで拡張します。 ユーザーは、ノーコードワークフローにカスタムステップを含めることができます。

ワークフローステップは以下を実行できます:

  • ユーザーから入力を収集
  • カスタムロジックを実行
  • 後続のステップのためにデータを出力

使用時期: Workflow Builder と統合する場合、非技術ユーザーが機能を使用できるようにする場合、再利用可能なオートメーション コンポーネントを構築する場合

from slack_bolt import App from slack_bolt.workflows.step import WorkflowStep import os

app = App( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"] )

ワークフローステップを定義

def edit(ack, step, configure): """ユーザーが Workflow Builder でステップを追加/編集するときに呼び出されます。""" ack()

# 設定モーダルを表示
blocks = [
    {
        "type": "input",
        "block_id": "ticket_type",
        "element": {
            "type": "static_select",
            "action_id": "type_select",
            "options": [
                {"text": {"type": "plain_text", "text": "Bug"}, "value": "bug"},
                {"text": {"type": "plain_text", "text": "Feature"}, "value": "feature"},
                {"text": {"type": "plain_text", "text": "Task"}, "value": "task"}
            ]
        },
        "label": {"type": "plain_text", "text": "Ticket Type"}
    },
    {
        "type": "input",
        "block_id": "title_input",
        "element": {
            "type": "plain_text_input",
            "action_id": "title"
        },
        "label": {"type": "plain_text", "text": "Title"}
    },
    {
        "type": "input",
        "block_id": "assignee_input",
        "element": {
            "type": "users_select",
            "action_id": "assignee"
        },
        "label": {"type": "plain_text", "text": "Assignee"}
    }
]

configure(blocks=blocks)

def save(ack, view, update): """ユーザーがステップ設定を保存するときに呼び出されます。""" ack()

values = view["state"]["values"]

# 入力を定義(ユーザーの設定から)
inputs = {
    "ticket_type": {
        "value": values["ticket_type"]["type_select"]["selected_option"]["value"]
    },
    "title": {
        "value": values["title_input"]["title"]["value"]
    },
    "assignee": {
        "value": values["assignee_input"]["assignee"]["selected_user"]
    }
}

# 出力を定義(後続のステップで利用可能)
outputs = [
    {
        "name": "ticket_id",
        "type": "text",
        "label": "Created Ticket ID"
    },
    {
        "name": "ticket_url",
        "type": "text",
        "label": "Ticket URL"
    }
]

update(inputs=inputs, outputs=outputs)

def execute(step, complete, fail): """ステップがワークフロー内で実行されるときに呼び出されます。""" inputs = step["inputs"]

try:
    # 入力値を取得
    ticket_type = inputs["ticket_type"]["value"]
    title = inputs["title"]["value"]
    assignee = inputs["assignee"]["value"]

    # システム内でチケットを作成
    ticket = create_ticket(
        type=ticket_type,
        title=title,
        assignee=assignee
    )

    # 出力で完了
    complete(outputs={
        "ticket_id": ticket["id"],
        "ticket_url": ticket["url"]
    })

except Exception as e:
    fail(error={"message": str(e)})

ワークフローステップを登録

create_ticket_step = WorkflowStep( callback_id="create_ticket_step", edit=edit, save=save, execute=execute )

app.step(create_ticket_step)

アンチパターン

  • execute で complete() または fail() を呼び出さない
  • 進捗更新のない長時間実行操作
  • execute で入力を検証しない
  • 出力に機密データを公開する

エッジケース

3 秒確認(タイムアウト)の欠落

重大度:CRITICAL(重大)

状況:スラッシュコマンド、ショートカット、またはインタラクティブコンポーネントを処理する

症状: ユーザーは「このコマンドはタイムアウトしました」または「問題が発生しました」と表示されます。 アクションは、コードが実行されていても完了しません。 開発では動作するが、本番では失敗します。

これが破損する理由: Slack は、すべてのインタラクティブリクエストに 3 秒以内の確認が必要です:

  • スラッシュコマンド
  • ボタン/選択メニュークリック
  • モーダル送信
  • ショートカット

応答する前に、遅い操作(データベース、API 呼び出し、LLM)を実行する場合、 ウィンドウを逃します。ボットが最終的にリクエストを正しく処理しても、 Slack はエラーを表示します。

推奨の修正:

即座に確認、後で処理

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import threading

app = App(token=os.environ["SLACK_BOT_TOKEN"])

@app.command("/slow-task")
def handle_slow_task(ack, command, client, respond):
    # 処理の前に即座に確認
    ack("Processing your request...")

    # バックグラウンドで遅い処理を実行
    def do_work():
        result = call_slow_api(command["text"])  # 10 秒かかる
        respond(f"Done! Result: {result}")

    threading.Thread(target=do_work).start()

@app.view("modal_submission")
def handle_modal(ack, body, client, view):
    # モーダルの場合は response_action で ack
    ack(response_action="clear")  # または "update" で新しいビュー

    # バックグラウンドで処理
    user_id = body["user"]["id"]
    values = view["state"]["values"]
    # ... 遅い処理

Bolt フレームワークの場合 - lazy リスナーを使用

# Bolt は lazy リスナーで ack() を自動的に処理
@app.command("/slow-task")
def handle_slow_task(ack, command, respond):
    ack()  # まだ ack() を呼び出す!

@handle_slow_task.lazy
def process_slow_task(command, respond):
    # これは ack 後に実行、必要な限り時間がかかる
    result = slow_operation(command["text"])
    respond(result)

OAuth State パラメータを検証しない(CSRF)

重大度:CRITICAL(重大)

状況:OAuth インストールフローを実装する

症状: ボットが動作しているように見えますが、CSRF 攻撃に対して脆弱です。 攻撃者がユーザーに悪意のある設定をインストールするよう騙すことができます。

これが破損する理由: OAuth state パラメータは CSRF 攻撃を防止します。フロー:

  1. ランダムな state を生成、保存、Slack に送信
  2. ユーザーが Slack で認可
  3. Slack は code + state でリダイレクト
  4. state が保存したものと一致することを確認する必要があります

これなく、攻撃者は悪意のある OAuth URL を作成でき、管理者が攻撃者の認可コードで フローを完了するよう騙すことができます。

推奨の修正:

適切な state 検証

import secrets
from flask import Flask, request, session, redirect
from slack_sdk.oauth import AuthorizeUrlGenerator
from slack_sdk.oauth.state_store import FileOAuthStateStore

app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]

# Slack SDK の state ストアを使用(本番環境では Redis 推奨)
state_store = FileOAuthStateStore(
    expiration_seconds=300,  # 5 分
    base_dir="./oauth_states"
)

@app.route("/slack/install")
def install():
    # 暗号化的にセキュアな state を生成
    state = state_store.issue()

    # セッションで検証用に保存
    session["oauth_state"] = state

    authorize_url = AuthorizeUrlGenerator(
        client_id=os.environ["SLACK_CLIENT_ID"],
        scopes=["channels:history", "chat:write"],
        user_scopes=[]
    ).generate(state)

    return redirect(authorize_url)

@app.route("/slack/oauth/callback")
def oauth_callback():
    # **重大:state を検証**
    received_state = request.args.get("state")
    stored_state = session.get("oauth_state")

    if not received_state or received_state != stored_state:
        return "Invalid state parameter - possible CSRF attack", 403

    # 1 回限りの使用のため state_store.consume() も使用
    if not state_store.consume(received_state):
        return "State already used or expired", 403

    # code をトークンと交換するのは安全
    code = request.args.get("code")
    # ... OAuth フロー完了

ボット/ユーザートークンを公開する

重大度:CRITICAL(重大)

状況:Slack トークンを保存またはログする

症状: 不正なメッセージがボットから送信されます。攻撃者がプライベートチャネルを読みます。 トークンが ログ、git 履歴、またはクライアント側コードに見つかります。

これが破損する理由: Slack トークンは、それらが持つスコープに対して完全なアクセスを提供します:

  • ボットトークン(xoxb-*):インストールされたワークスペースへのアクセス
  • ユーザートークン(xoxp-*):その特定のユーザーとしてアクセス
  • アプリレベルトークン(xapp-*):Socket Mode 接続

一般的な公開ポイント:

  • ソースコードにハードコード
  • エラーメッセージでログ
  • フロントエンド/クライアントに送信
  • 暗号化なしでデータベースに保存

推奨の修正:

トークンをハードコードまたはログしない

# 悪い - これをしないでください
client = WebClient(token="xoxb-12345-...")

# 良い - 環境変数
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])

# 悪い - トークンをログ
logger.error(f"API call failed with token {token}")

# 良い - トークンをログしない
logger.error(f"API call failed for team {team_id}")

# 悪い - トークンをフロントエンドに送信
return {"token": bot_token}

# 良い - フロントエンドが必要なものだけを送信
return {"channels": channel_list}

データベース内のトークンを暗号化

from cryptography.fernet import Fernet

class TokenStore:
    def __init__(self, encryption_key: str):
        self.cipher = Fernet(encryption_key)

    def save_token(self, team_id: str, token: str):
        encrypted = self.cipher.encrypt(token.encode())
        db.execute(
            "INSERT INTO installations (team_id, encrypted_token) VALUES (?, ?)",
            (team_id, encrypted)
        )

    def get_token(self, team_id: str) -> str:
        row = db.execute(
            "SELECT encrypted_token FROM installations WHERE team_id = ?",
            (team_id,)
        ).fetchone()
        return self.cipher.decrypt(row[0]).decode()

トークンが公開された場合はローテーション

1. Slack API > Your App > OAuth & Permissions
2. 公開されたトークンの「Rotate」をクリック
3. すべてのデプロイを即座に更新
4. 不正なアクセスについて Slack 監査ログを確認

不要な OAuth スコープをリクエスト

重大度:HIGH(高)

状況:アプリの OAuth スコープを設定する

症状: 権限警告が怖いため、ユーザーはインストールに躊躇します。 インストール率が低い。セキュリティチームがデプロイをブロック。 アプリが Slack App Directory から却下されます。

これが破損する理由: 各 OAuth スコープは特定の権限を付与します。必要以上に要求する:

  • インストール同意画面をおどろおどろしくする
  • トークンリーク時の攻撃面を増やす
  • エンタープライズセキュリティポリシーに違反する可能性
  • Slack App Directory からアプリが却下される可能性

一般的な過剰なリクエスト:

  • chat:write だけで済むのに admin
  • 1 つのチャネルにメッセージするだけなのに channels:read
  • メールが必要ないのに users:read.email

推奨の修正:

最小限の必要なスコープをリクエスト

# シンプルな通知ボット
MINIMAL_SCOPES = [
    "chat:write",        # メッセージを投稿
    "channels:join",     # 公開チャネルに参加(必要な場合)
]

# 基本的な通知には不要:
# - channels:read(チャネルを一覧表示しない限り)
# - users:read(ユーザーを検索しない限り)
# - channels:history(メッセージを読まない限り)

# スラッシュコマンドボット
SLASH_COMMAND_SCOPES = [
    "commands",          # スラッシュコマンドを登録
    "chat:write",        # コマンドに応答
]

# メンションに応答するボット
MENTION_BOT_SCOPES = [
    "app_mentions:read", # @mentions を受け取る
    "chat:write",        # メンションに返信
]

ユースケース別スコープリファレンス

ユースケース必要なスコープ
メッセージを投稿chat:write
スラッシュコマンドcommands
@mentions に応答app_mentions:read, chat:write
チャネルメッセージを読むchannels:history(公開)、groups:history(非公開)
ユーザー情報を読むusers:read
モーダルを開くcommands またはイベントからトリガー
リアクションを追加reactions:write
ファイルをアップロードfiles:write

プログレッシブなスコープリクエスト

# 最小スコープで開始
INITIAL_SCOPES = ["chat:write", "commands"]

# 必要な場合のみ追加スコープをリクエスト
@app.command("/enable-reactions")
def enable_reactions(ack, client, command):
    ack()

    # スコープを持っているか確認
    auth_result = client.auth_test()
    # reactions:write が欠落している場合、再認可をプロンプト
    if needs_additional_scope:
        # ユーザーを追加スコープで再認可に送信
        pass

Block Kit リミットを超える

重大度:MEDIUM(中)

状況:Block Kit を使用して複雑なメッセージ UI を構築する

症状: メッセージが「invalid_blocks」エラーで送信に失敗します。 モーダルが開きません。メッセージが予期せず切り詰められます。

これが破損する理由: Block Kit には厳密なリミットがあり、常に明白ではありません:

  • メッセージ/モーダルあたり 50 ブロック
  • テキストブロックあたり 3000 文字
  • アクションブロックあたり 10 要素
  • 選択メニューあたり 100 オプション
  • モーダル:50 ブロック、合計 24KB
  • Home タブ:100 ブロック

これらの超過はサイレント失敗または暗号のようなエラーを引き起こします。

推奨の修正:

リミットを知って尊重する

# Block Kit リミット用の定数
BLOCK_KIT_LIMITS = {
    "blocks_per_message": 50,
    "blocks_per_modal": 50,
    "blocks_per_home": 100,
    "text_block_chars": 3000,
    "elements_per_actions": 10,
    "options_per_select": 100,
    "modal_total_bytes": 24 * 1024,  # 24KB
}

def validate_blocks(blocks: list) -> tuple[bool, str]:
    """送信前にブロックを検証します。"""
    if len(blocks) > BLOCK_KIT_LIMITS["blocks_per_message"]:
        return False, f"Too many blocks: {len(blocks)} > 50"

    for block in blocks:
        if block.get("type") == "section":
            text = block.get("text", {}).get("text", "")
            if len(text) > BLOCK_KIT_LIMITS["text_block_chars"]:
                return False, f"Text too long: {len(text)} > 3000"

        if block.get("type") == "actions":
            elements = block.get("elements", [])
            if len(elements) > BLOCK_KIT_LIMITS["elements_per_actions"]:
                return False, f"Too many actions: {len(elements)} > 10"

    return True, "OK"

# 長いコンテンツをページネート
def paginate_blocks(blocks: list, page: int = 0, per_page: int = 45):
    """ナビゲーション付きでブロックをページネートします。"""
    start = page * per_page
    end = start + per_page
    page_blocks = blocks[start:end]

    # ページネーションコントロールを追加
    if len(blocks) > per_page:
        page_blocks.append({
            "type": "actions",
            "elements": [
                {"type": "button", "text": {"type": "plain_text", "text": "Previous"},
                 "action_id": f"page_{page-1}", "disabled": page == 0},
                {"type": "button", "text": {"type": "plain_text", "text": "Next"},
                 "action_id": f"page_{page+1}",
                 "disabled": end >= len(blocks)}
            ]
        })

    return page_blocks

本番環境で Socket Mode を使用

重大度:HIGH(高)

状況:Slack ボットを本番環境にデプロイ

症状: ボットは開発では動作するが、本番では信頼できません。 イベントを逃します。接続がドロップします。水平方向にスケーリングできません。

これが破損する理由: Socket Mode は開発用に設計されています:

  • アプリあたり単一の WebSocket 接続
  • 複数インスタンスにスケーリングできない
  • 接続がドロップする可能性(再接続ロジックが必要)
  • 組み込みのロードバランシングなし

複数インスタンスまたは高トラフィックの本番環境では、 HTTP ウェブフックがより信頼できます。

推奨の修正:

Socket Mode:開発のみ

# Socket Mode での開発
if os.environ.get("ENVIRONMENT") == "development":
    from slack_bolt.adapter.socket_mode import SocketModeHandler
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

本番環境:HTTP エンドポイントを使用

# Flask を使用した HTTP での本番環境
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request

flask_app = Flask(__name__)
handler = SlackRequestHandler(app)

@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
    return handler.handle(request)

@flask_app.route("/slack/commands", methods=["POST"])
def slack_commands():
    return handler.handle(request)

@flask_app.route("/slack/interactions", methods=["POST"])
def slack_interactions():
    return handler.handle(request)

本番環境で Socket Mode を使用する必要がある場合

from slack_bolt.adapter.socket_mode import SocketModeHandler
import time

class RobustSocketHandler:
    def __init__(self, app, app_token):
        self.app = app
        self.app_token = app_token
        self.handler = None

    def start(self):
        while True:
            try:
                self.handler = SocketModeHandler(self.app, self.app_token)
                self.handler.start()
            except Exception as e:
                logger.error(f"Socket Mode disconnected: {e}")
                time.sleep(5)  # 再接続前にバックオフ

リクエスト署名を検証しない

重大度:CRITICAL(重大)

状況:Slack からウェブフックを受け取る

症状: 攻撃者はウェブフックエンドポイントに偽のリクエストを送信できます。 スプーフされたスラッシュコマンド。処理される偽のイベント通知。

これが破損する理由: Slack は X-Slack-Signature ヘッダーを使用して署名秘密ですべてのリクエストに署名します。 検証なく、ウェブフック URL を知っている誰もが偽のリクエストを送信できます。

これは OAuth トークンと異なります - 署名はリクエストが Slack から来たことを検証し、Slack を呼び出す権限を持っていることではありません。

推奨の修正:

Bolt が自動的に処理

from slack_bolt import App

# signing_secret を提供するとき、Bolt は署名を自動的に検証
app = App(
    token=os.environ["SLACK_BOT_TOKEN"],
    signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
# ハンドラへのすべてのリクエストが検証されます

手動検証(Bolt を使用しない場合)

import hmac
import hashlib
import time
from flask import Flask, request, abort

SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]

def verify_slack_signature(request):
    timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
    signature = request.headers.get("X-Slack-Signature", "")

    # 古いタイムスタンプを拒否(リプレイ攻撃防止)
    if abs(time.time() - int(timestamp)) > 60 * 5:
        return False

    # 予期される署名を計算
    sig_basestring = f"v0:{timestamp}:{request.get_data(as_text=True)}"
    expected_sig = "v0=" + hmac.new(
        SIGNING_SECRET.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()

    # 定時間比較
    return hmac.compare_digest(expected_sig, signature)

@app.route("/slack/events", methods=["POST"])
def slack_events():
    if not verify_slack_signature(request):
        abort(403)
    # 安全に処理

検証チェック

ハードコード Slack トークン

重大度:ERROR

Slack トークンはハードコードしてはいけません

メッセージ:ハードコード Slack トークンが検出されました。環境変数を使用してください。

ソースコード内の署名秘密

重大度:ERROR

署名秘密は環境変数に置くべき

メッセージ:ハードコード署名秘密。os.environ['SLACK_SIGNING_SECRET'] を使用してください。

署名検証なしの Webhook

重大度:ERROR

Slack ウェブフックは X-Slack-Signature を検証する必要があります

メッセージ:署名検証なしの Webhook。Bolt を使用するか手動で検証してください。

クライアント側コード内の Slack トークン

重大度:ERROR

Slack トークンをブラウザに公開しない

メッセージ:Slack 認証情報がクライアント側に公開されています。サーバー側のみで使用。

確認前の遅い操作

重大度:WARNING

ack() は遅い操作の前に呼び出す必要があります

メッセージ:ack() 前の遅い操作。最初に ack() を呼び出してから処理してください。

確認呼び出しの欠落

重大度:WARNING

インタラクティブハンドラは ack() を呼び出す必要があります

メッセージ:ハンドラで ack() 呼び出しが欠落しています。3 秒以内に確認する必要があります。

State 検証なしの OAuth

重大度:ERROR

OAuth コールバックは state パラメータを検証する必要があります

メッセージ:state 検証なしの OAuth。CSRF 攻撃に脆弱です。

暗号化なしのトークンストレージ

重大度:WARNING

トークンは保存時に暗号化する必要があります

メッセージ:トークンが暗号化なしで保存されています。保存時に暗号化してください。

Admin スコープをリクエスト

重大度:WARNING

絶対に必要でない限り admin スコープを避けてください

メッセージ:admin スコープをリクエスト中。最小限の必要なスコープを使用してください。

潜在的に未使用なスコープ

重大度:INFO

リクエストされているすべてのスコープが使用されているか確認してください

メッセージ:users:read.email をリクエスト中ですが、メール を使用しない可能性があります。必要性を確認してください。

コラボレーション

デリゲーショントリガー

  • AI パワーの Slack ボットが必要 -> llm-architect (会話型 Slack ボット用に LLM を統合)
  • 音声通知が必要 -> twilio-communications (Slack アラートを SMS または音声通話にエスカレート)
  • ワークフロー自動化が必要 -> workflow-automation (n8n/Temporal ワークフローのトリガー/アクションとしての Slack)
  • Discord 用のボットも必要 -> discord-bot-architect (クロスプラットフォームボットアーキテクチャ)
  • 完全な認証システムが必要 -> auth-specialist (OAuth、ワークスペース管理、エンタープライズ SSO)
  • ボットデータ用のデータベースが必要 -> postgres-wizard (インストール、ユーザー設定、メッセージ履歴を保存)
  • 高可用性が必要 -> devops (ウェブフックのスケール、監視、アラート)

使用時期

  • ユーザーが言及または暗示:slack bot
  • ユーザーが言及または暗示:slack app
  • ユーザーが言及または暗示:bolt framework
  • ユーザーが言及または暗示:block kit
  • ユーザーが言及または暗示:slash command
  • ユーザーが言及または暗示:slack webhook
  • ユーザーが言及または暗示:slack workflow
  • ユーザーが言及または暗示:slack interactive
  • ユーザーが言及または暗示:slack oauth

制限事項

  • 上記で説明したスコープと明確に一致する場合のみこのスキルを使用してください。
  • 出力を環境固有の検証、テスト、または専門家による審査の代替としないでください。
  • 必要な入力、権限、セキュリティ境界、または成功基準が不足している場合は停止して明確化を求めてください。

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
sickn33
リポジトリ
sickn33/antigravity-awesome-skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: sickn33 · sickn33/antigravity-awesome-skills · ライセンス: MIT