diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 434ef3a..6e77d81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,7 @@ jobs: - "ai/slackbot-mcp-client/rich-responses/mcp-apps" - "ai/slackbot-mcp-client/slack-identity" - "block-kit" + - "methods" steps: - name: Checkout code uses: actions/checkout@v7 diff --git a/README.md b/README.md index 5a58c76..8fe0a2e 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ This collections of examples highlights features of a Slack app in the language - **[AI in Slack](./ai)**: Agent experiences and MCP features in an interactive conversation interface. - **[Block Kit](./block-kit)**: The framework of visual components arranged to create app layouts. +- **[Methods](./methods)**: Individual Slack Web API method calls with the `slack_sdk` `WebClient`. diff --git a/methods/.gitignore b/methods/.gitignore new file mode 100644 index 0000000..4a797c4 --- /dev/null +++ b/methods/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.mypy_cache +.pytest_cache +.ruff_cache +.venv diff --git a/methods/README.md b/methods/README.md new file mode 100644 index 0000000..07f419a --- /dev/null +++ b/methods/README.md @@ -0,0 +1,20 @@ +# Methods + +Individual Slack Web API method calls with the `slack_sdk` `WebClient`. + +Read the [docs](https://docs.slack.dev/reference/methods) to explore every method, or explore implementations of specific families. + +## What's on display + +### chat + +- **[chat.postMessage](https://docs.slack.dev/reference/methods/chat.postmessage)**: Sends a message to a channel. [Implementation](./src/chat/chat_post_message.py). + +## Running an example + +Each family ships a [`manifest.json`](./src/chat/manifest.json) requesting only the scopes it needs (`chat` → `chat:write`). Create an app from it, then set a bot token and run an example module directly: + +```sh +export SLACK_TOKEN="xoxb-your-token" +python -m src.chat.chat_post_message +``` diff --git a/methods/requirements.txt b/methods/requirements.txt new file mode 100644 index 0000000..b297075 --- /dev/null +++ b/methods/requirements.txt @@ -0,0 +1,4 @@ +mypy==2.3.0 +pytest==9.1.1 +ruff==0.15.21 +slack_sdk==3.43.0 diff --git a/methods/src/__init__.py b/methods/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/methods/src/chat/__init__.py b/methods/src/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/methods/src/chat/chat_post_message.py b/methods/src/chat/chat_post_message.py new file mode 100644 index 0000000..ffaaabb --- /dev/null +++ b/methods/src/chat/chat_post_message.py @@ -0,0 +1,27 @@ +import os + +from slack_sdk import WebClient +from slack_sdk.web import SlackResponse + + +def example01(client: WebClient) -> SlackResponse: + """ + Sends a message to a channel. + https://docs.slack.dev/reference/methods/chat.postmessage + """ + # Call the chat.postMessage method using the WebClient + response = client.chat_postMessage( + channel="C123ABC456", + text="Here's a message for you", + ) + return response + + +if __name__ == "__main__": + # Read a token from the environment variables + token = os.environ.get("SLACK_TOKEN") + + # Initialize a WebClient with the given token + client = WebClient(token=token) + + print(example01(client)) diff --git a/methods/src/chat/manifest.json b/methods/src/chat/manifest.json new file mode 100644 index 0000000..ad13ee2 --- /dev/null +++ b/methods/src/chat/manifest.json @@ -0,0 +1,22 @@ +{ + "display_information": { + "name": "Slack API Methods", + "description": "Example implementations to call \"chat\" methods" + }, + "features": { + "bot_user": { + "display_name": "Slack API Methods", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": ["chat:write"] + } + }, + "settings": { + "org_deploy_enabled": true, + "socket_mode_enabled": false, + "token_rotation_enabled": false + } +} diff --git a/methods/tests/__init__.py b/methods/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/methods/tests/chat/__init__.py b/methods/tests/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/methods/tests/chat/test_chat_post_message.py b/methods/tests/chat/test_chat_post_message.py new file mode 100644 index 0000000..7504d21 --- /dev/null +++ b/methods/tests/chat/test_chat_post_message.py @@ -0,0 +1,43 @@ +import json +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + +from slack_sdk import WebClient + +from src.chat import chat_post_message + + +class _CapturingHandler(BaseHTTPRequestHandler): + captured: dict = {} + + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + _CapturingHandler.captured = { + "path": self.path, + "body": self.rfile.read(length).decode(), + } + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(b'{"ok": true}') + + def log_message(self, *args): + pass + + +def test_example01(): + server = HTTPServer(("127.0.0.1", 0), _CapturingHandler) + port = server.server_address[1] + threading.Thread(target=server.handle_request, daemon=True).start() + + client = WebClient(token="xoxb-test", base_url=f"http://127.0.0.1:{port}/api/") + response = chat_post_message.example01(client) + + assert response["ok"] is True + + captured = _CapturingHandler.captured + assert captured["path"] == "/api/chat.postMessage" + assert json.loads(captured["body"]) == { + "channel": "C123ABC456", + "text": "Here's a message for you", + }