diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 90ffbc6b7..351f9257f 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -25,7 +25,7 @@ jobs:
run: |
python setup.py install
pip install -U pip
- pip install "pytest>=5,<6" "pytest-cov>=2,<3"
+ pip install "pytest>=5,<6" "pytest-cov>=2,<3" "flask_sockets>0.2,<1"
- name: Run tests without aiohttp
run: |
pytest tests/slack_bolt/
diff --git a/codecov.yml b/codecov.yml
index b24c2afb1..5568e5e6b 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -2,7 +2,7 @@ coverage:
status:
project:
default:
- threshold: 0.3%
+ threshold: 2.0%
patch:
default:
target: 50%
diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md
new file mode 100644
index 000000000..323a5e6be
--- /dev/null
+++ b/docs/_basic/socket_mode.md
@@ -0,0 +1,55 @@
+---
+title: Using Socket Mode
+lang: en
+slug: socket-mode
+order: 16
+---
+
+
+With the introduction of [Socket Mode](https://api.slack.com/socket-mode), Bolt for Python introduced support in version `1.2.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a WebSocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings.
+
+To use the Socket Mode, add `SLACK_APP_TOKEN` as an environment variable. You can get your App Token in your app configuration settings under the **Basic Information** section.
+
+
+```python
+import os
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode import SocketModeHandler
+
+# Install the Slack app and get xoxb- token in advance
+app = App(token=os.environ["SLACK_BOT_TOKEN"])
+
+if __name__ == "__main__":
+ # export SLACK_APP_TOKEN=xapp-***
+ # export SLACK_BOT_TOKEN=xoxb-***
+ SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
+```
+
+While we recommend using [the built-in Socket Mode adapter](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter/socket_mode/builtin), there are a few other 3rd party library based implementations. Here is the list of available adapters.
+
+|PyPI Project|Bolt Adapter|
+|-|-|
+|[slack_sdk](https://pypi.org/project/slack-sdk/)|[slack_bolt.adapter.socket_mode](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter/socket_mode/builtin)|
+|[websocket_client](https://pypi.org/project/websocket_client/)|[slack_bolt.adapter.socket_mode.websocket_client](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter/socket_mode/websocket_client)|
+|[aiohttp](https://pypi.org/project/aiohttp/) (asyncio-based)|[slack_bolt.adapter.socket_mode.aiohttp](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter/socket_mode/aiohttp)|
+|[websockets](https://pypi.org/project/websockets/) (asyncio-based)|[slack_bolt.adapter.socket_mode.websockets](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter/socket_mode/websockets)|
+
+To use the asyncio-based adapters such as aiohttp, your app needs to be compatible with asyncio's async/await programming model. `AsyncSocketModeHandler` is available for running `AsyncApp` and its async middleware and listeners.
+
+```python
+from slack_bolt.app.async_app import AsyncApp
+# The default is the aiohttp based implementation
+from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
+
+app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"])
+
+async def main():
+ handler = AsyncSocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
+ await handler.start_async()
+
+if __name__ == "__main__":
+ import asyncio
+ asyncio.run(main())
+```
+
+To learn how to use `AsyncApp`, checkout the [Using Async](https://slack.dev/bolt-python/concepts#async) document and relevant [examples](https://github.com/slackapi/bolt-python/tree/main/examples).
diff --git a/examples/socket_mode.py b/examples/socket_mode.py
new file mode 100644
index 000000000..e4b2f10ba
--- /dev/null
+++ b/examples/socket_mode.py
@@ -0,0 +1,102 @@
+# ------------------------------------------------
+# instead of slack_bolt in requirements.txt
+import sys
+
+sys.path.insert(1, "..")
+# ------------------------------------------------
+
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+
+import os
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode import SocketModeHandler
+
+# Install the Slack app and get xoxb- token in advance
+app = App(token=os.environ["SLACK_BOT_TOKEN"])
+
+
+@app.command("/hello-socket-mode")
+def hello_command(ack, body):
+ user_id = body["user_id"]
+ ack(f"Hi <@{user_id}>!")
+
+
+@app.event("app_mention")
+def event_test(event, say):
+ say(f"Hi there, <@{event['user']}>!")
+
+
+def ack_shortcut(ack):
+ ack()
+
+
+def open_modal(body, client):
+ client.views_open(
+ trigger_id=body["trigger_id"],
+ view={
+ "type": "modal",
+ "callback_id": "socket_modal_submission",
+ "submit": {"type": "plain_text", "text": "Submit",},
+ "close": {"type": "plain_text", "text": "Cancel",},
+ "title": {"type": "plain_text", "text": "Socket Modal",},
+ "blocks": [
+ {
+ "type": "input",
+ "block_id": "q1",
+ "label": {"type": "plain_text", "text": "Write anything here!",},
+ "element": {"action_id": "feedback", "type": "plain_text_input",},
+ },
+ {
+ "type": "input",
+ "block_id": "q2",
+ "label": {
+ "type": "plain_text",
+ "text": "Can you tell us your favorites?",
+ },
+ "element": {
+ "type": "external_select",
+ "action_id": "favorite-animal",
+ "min_query_length": 0,
+ "placeholder": {
+ "type": "plain_text",
+ "text": "Select your favorites",
+ },
+ },
+ },
+ ],
+ },
+ )
+
+
+app.shortcut("socket-mode")(ack=ack_shortcut, lazy=[open_modal])
+
+
+all_options = [
+ {"text": {"type": "plain_text", "text": ":cat: Cat"}, "value": "cat",},
+ {"text": {"type": "plain_text", "text": ":dog: Dog"}, "value": "dog",},
+ {"text": {"type": "plain_text", "text": ":bear: Bear"}, "value": "bear",},
+]
+
+
+@app.options("favorite-animal")
+def external_data_source_handler(ack, body):
+ keyword = body.get("value")
+ if keyword is not None and len(keyword) > 0:
+ options = [o for o in all_options if keyword in o["text"]["text"]]
+ ack(options=options)
+ else:
+ ack(options=all_options)
+
+
+@app.view("socket_modal_submission")
+def submission(ack):
+ ack()
+
+
+if __name__ == "__main__":
+ # export SLACK_APP_TOKEN=xapp-***
+ # export SLACK_BOT_TOKEN=xoxb-***
+ SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
diff --git a/examples/socket_mode_async.py b/examples/socket_mode_async.py
new file mode 100644
index 000000000..99bb0ae69
--- /dev/null
+++ b/examples/socket_mode_async.py
@@ -0,0 +1,112 @@
+# ------------------------------------------------
+# instead of slack_bolt in requirements.txt
+import sys
+
+
+sys.path.insert(1, "..")
+# ------------------------------------------------
+
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+
+import os
+
+from slack_bolt.app.async_app import AsyncApp
+from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
+
+# Install the Slack app and get xoxb- token in advance
+app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"])
+
+
+@app.command("/hello-socket-mode")
+async def hello_command(ack, body):
+ user_id = body["user_id"]
+ await ack(f"Hi <@{user_id}>!")
+
+
+@app.event("app_mention")
+async def event_test(event, say):
+ await say(f"Hi there, <@{event['user']}>!")
+
+
+async def ack_shortcut(ack):
+ await ack()
+
+
+async def open_modal(body, client):
+ await client.views_open(
+ trigger_id=body["trigger_id"],
+ view={
+ "type": "modal",
+ "callback_id": "socket_modal_submission",
+ "submit": {"type": "plain_text", "text": "Submit",},
+ "close": {"type": "plain_text", "text": "Cancel",},
+ "title": {"type": "plain_text", "text": "Socket Modal",},
+ "blocks": [
+ {
+ "type": "input",
+ "block_id": "q1",
+ "label": {"type": "plain_text", "text": "Write anything here!",},
+ "element": {"action_id": "feedback", "type": "plain_text_input",},
+ },
+ {
+ "type": "input",
+ "block_id": "q2",
+ "label": {
+ "type": "plain_text",
+ "text": "Can you tell us your favorites?",
+ },
+ "element": {
+ "type": "external_select",
+ "action_id": "favorite-animal",
+ "min_query_length": 0,
+ "placeholder": {
+ "type": "plain_text",
+ "text": "Select your favorites",
+ },
+ },
+ },
+ ],
+ },
+ )
+
+
+app.shortcut("socket-mode")(ack=ack_shortcut, lazy=[open_modal])
+
+
+all_options = [
+ {"text": {"type": "plain_text", "text": ":cat: Cat"}, "value": "cat",},
+ {"text": {"type": "plain_text", "text": ":dog: Dog"}, "value": "dog",},
+ {"text": {"type": "plain_text", "text": ":bear: Bear"}, "value": "bear",},
+]
+
+
+@app.options("favorite-animal")
+async def external_data_source_handler(ack, body):
+ keyword = body.get("value")
+ if keyword is not None and len(keyword) > 0:
+ options = [o for o in all_options if keyword in o["text"]["text"]]
+ await ack(options=options)
+ else:
+ await ack(options=all_options)
+
+
+@app.view("socket_modal_submission")
+async def submission(ack):
+ await ack()
+
+
+# export SLACK_APP_TOKEN=xapp-***
+# export SLACK_BOT_TOKEN=xoxb-***
+
+
+async def main():
+ handler = AsyncSocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
+ await handler.start_async()
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ asyncio.run(main())
diff --git a/examples/socket_mode_oauth.py b/examples/socket_mode_oauth.py
new file mode 100644
index 000000000..fd51b3150
--- /dev/null
+++ b/examples/socket_mode_oauth.py
@@ -0,0 +1,116 @@
+# ------------------------------------------------
+# instead of slack_bolt in requirements.txt
+import sys
+
+sys.path.insert(1, "..")
+# ------------------------------------------------
+
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+
+import os
+from slack_bolt.app import App
+from slack_bolt.oauth.oauth_settings import OAuthSettings
+from slack_bolt.adapter.socket_mode import SocketModeHandler
+
+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=os.environ["SLACK_SCOPES"].split(","),
+ ),
+)
+
+
+@app.command("/hello-socket-mode")
+def hello_command(ack, body):
+ user_id = body["user_id"]
+ ack(f"Hi <@{user_id}>!")
+
+
+@app.event("app_mention")
+def event_test(event, say):
+ say(f"Hi there, <@{event['user']}>!")
+
+
+def ack_shortcut(ack):
+ ack()
+
+
+def open_modal(body, client):
+ client.views_open(
+ trigger_id=body["trigger_id"],
+ view={
+ "type": "modal",
+ "callback_id": "socket_modal_submission",
+ "submit": {"type": "plain_text", "text": "Submit",},
+ "close": {"type": "plain_text", "text": "Cancel",},
+ "title": {"type": "plain_text", "text": "Socket Modal",},
+ "blocks": [
+ {
+ "type": "input",
+ "block_id": "q1",
+ "label": {"type": "plain_text", "text": "Write anything here!",},
+ "element": {"action_id": "feedback", "type": "plain_text_input",},
+ },
+ {
+ "type": "input",
+ "block_id": "q2",
+ "label": {
+ "type": "plain_text",
+ "text": "Can you tell us your favorites?",
+ },
+ "element": {
+ "type": "external_select",
+ "action_id": "favorite-animal",
+ "min_query_length": 0,
+ "placeholder": {
+ "type": "plain_text",
+ "text": "Select your favorites",
+ },
+ },
+ },
+ ],
+ },
+ )
+
+
+app.shortcut("socket-mode")(ack=ack_shortcut, lazy=[open_modal])
+
+
+all_options = [
+ {"text": {"type": "plain_text", "text": ":cat: Cat"}, "value": "cat",},
+ {"text": {"type": "plain_text", "text": ":dog: Dog"}, "value": "dog",},
+ {"text": {"type": "plain_text", "text": ":bear: Bear"}, "value": "bear",},
+]
+
+
+@app.options("favorite-animal")
+def external_data_source_handler(ack, body):
+ keyword = body.get("value")
+ if keyword is not None and len(keyword) > 0:
+ options = [o for o in all_options if keyword in o["text"]["text"]]
+ ack(options=options)
+ else:
+ ack(options=all_options)
+
+
+@app.view("socket_modal_submission")
+def submission(ack):
+ ack()
+
+
+if __name__ == "__main__":
+ SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).connect()
+ app.start()
+
+ # export SLACK_APP_TOKEN=
+ # export SLACK_SIGNING_SECRET=
+ # export SLACK_CLIENT_ID=
+ # export SLACK_CLIENT_SECRET=
+ # export SLACK_SCOPES=
+ # pip install .[optional]
+ # pip install slack_bolt
+ # python socket_mode_oauth.py
diff --git a/scripts/uninstall_all.sh b/scripts/uninstall_all.sh
index 188f97a41..1d3da265d 100755
--- a/scripts/uninstall_all.sh
+++ b/scripts/uninstall_all.sh
@@ -1,3 +1,4 @@
#!/bin/bash
-pip freeze | grep -v "^-e" | xargs pip uninstall -y
\ No newline at end of file
+pip uninstall -y slack-bolt && \
+ pip freeze | grep -v "^-e" | xargs pip uninstall -y
diff --git a/setup.py b/setup.py
index cb789759a..b2ba603d1 100755
--- a/setup.py
+++ b/setup.py
@@ -16,6 +16,7 @@
"pytest-cov>=2,<3",
"pytest-asyncio<1", # for async
"aiohttp>=3,<4", # for async
+ "Flask-Sockets>=0.2,<1",
"black==20.8b1",
]
@@ -33,7 +34,7 @@
exclude=["examples", "integration_tests", "tests", "tests.*",]
),
include_package_data=True, # MANIFEST.in
- install_requires=["slack_sdk>=3.1.1,<3.2",],
+ install_requires=["slack_sdk>=3.2.0rc2,<3.3",],
setup_requires=["pytest-runner==5.2"],
tests_require=test_dependencies,
test_suite="tests",
diff --git a/slack_bolt/adapter/socket_mode/__init__.py b/slack_bolt/adapter/socket_mode/__init__.py
new file mode 100644
index 000000000..94223be4d
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/__init__.py
@@ -0,0 +1,2 @@
+# Don't add async module imports here
+from .builtin import SocketModeHandler # noqa
diff --git a/slack_bolt/adapter/socket_mode/aiohttp/__init__.py b/slack_bolt/adapter/socket_mode/aiohttp/__init__.py
new file mode 100644
index 000000000..0e3038c75
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/aiohttp/__init__.py
@@ -0,0 +1,80 @@
+import os
+from logging import Logger
+from time import time
+from typing import Optional
+
+from slack_sdk.socket_mode.aiohttp import SocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.web.async_client import AsyncWebClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode.async_base_handler import AsyncBaseSocketModeHandler
+from slack_bolt.adapter.socket_mode.async_internals import (
+ send_async_response,
+ run_async_bolt_app,
+)
+from slack_bolt.adapter.socket_mode.internals import run_bolt_app
+from slack_bolt.app.async_app import AsyncApp
+from slack_bolt.response import BoltResponse
+
+
+class SocketModeHandler(AsyncBaseSocketModeHandler):
+ app: App # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: App, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[AsyncWebClient] = None,
+ proxy: Optional[str] = None,
+ ping_interval: float = 10,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ proxy=proxy,
+ ping_interval=ping_interval,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = run_bolt_app(self.app, req)
+ await send_async_response(client, req, bolt_resp, start)
+
+
+class AsyncSocketModeHandler(AsyncBaseSocketModeHandler):
+ app: AsyncApp # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: AsyncApp, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[AsyncWebClient] = None,
+ proxy: Optional[str] = None,
+ ping_interval: float = 10,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ proxy=proxy,
+ ping_interval=ping_interval,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = await run_async_bolt_app(self.app, req)
+ await send_async_response(client, req, bolt_resp, start)
diff --git a/slack_bolt/adapter/socket_mode/async_base_handler.py b/slack_bolt/adapter/socket_mode/async_base_handler.py
new file mode 100644
index 000000000..836074fad
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/async_base_handler.py
@@ -0,0 +1,37 @@
+import asyncio
+import logging
+from typing import Union
+
+from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+
+from slack_bolt import App
+from slack_bolt.app.async_app import AsyncApp
+from slack_bolt.util.utils import get_boot_message
+
+
+class AsyncBaseSocketModeHandler:
+ app: Union[App, AsyncApp] # type: ignore
+ client: AsyncBaseSocketModeClient
+
+ async def handle(
+ self, client: AsyncBaseSocketModeClient, req: SocketModeRequest
+ ) -> None:
+ raise NotImplementedError()
+
+ async def connect_async(self):
+ await self.client.connect()
+
+ async def disconnect_async(self):
+ await self.client.disconnect()
+
+ async def close_async(self):
+ await self.client.close()
+
+ async def start_async(self):
+ await self.connect_async()
+ if self.app.logger.level > logging.INFO:
+ print(get_boot_message())
+ else:
+ self.app.logger.info(get_boot_message())
+ await asyncio.sleep(float("inf"))
diff --git a/slack_bolt/adapter/socket_mode/async_handler.py b/slack_bolt/adapter/socket_mode/async_handler.py
new file mode 100644
index 000000000..08edd8004
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/async_handler.py
@@ -0,0 +1 @@
+from .aiohttp import AsyncSocketModeHandler # noqa
diff --git a/slack_bolt/adapter/socket_mode/async_internals.py b/slack_bolt/adapter/socket_mode/async_internals.py
new file mode 100644
index 000000000..5b5750bd1
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/async_internals.py
@@ -0,0 +1,50 @@
+import json
+import logging
+from time import time
+
+from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.socket_mode.response import SocketModeResponse
+
+from slack_bolt.app.async_app import AsyncApp
+from slack_bolt.request.async_request import AsyncBoltRequest
+from slack_bolt.response import BoltResponse
+
+
+async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest): # type: ignore
+ bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload)
+ bolt_resp: BoltResponse = await app.async_dispatch(bolt_req)
+ return bolt_resp
+
+
+async def send_async_response(
+ client: AsyncBaseSocketModeClient,
+ req: SocketModeRequest,
+ bolt_resp: BoltResponse,
+ start_time: float,
+):
+ if bolt_resp.status == 200:
+ content_type = bolt_resp.headers.get("content-type", [""])[0]
+ if bolt_resp.body is None or len(bolt_resp.body) == 0:
+ await client.send_socket_mode_response(
+ SocketModeResponse(envelope_id=req.envelope_id)
+ )
+ elif content_type.startswith("application/json"):
+ dict_body = json.loads(bolt_resp.body)
+ await client.send_socket_mode_response(
+ SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)
+ )
+ else:
+ await client.send_socket_mode_response(
+ SocketModeResponse(
+ envelope_id=req.envelope_id,
+ payload={"text": bolt_resp.body},
+ )
+ )
+ if client.logger.level <= logging.DEBUG:
+ spent_time = int((time() - start_time) * 1000)
+ client.logger.debug(f"Response time: {spent_time} milliseconds")
+ else:
+ client.logger.info(
+ f"Unsuccessful Bolt execution result (status: {bolt_resp.status}, body: {bolt_resp.body})"
+ )
diff --git a/slack_bolt/adapter/socket_mode/base_handler.py b/slack_bolt/adapter/socket_mode/base_handler.py
new file mode 100644
index 000000000..99568754e
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/base_handler.py
@@ -0,0 +1,33 @@
+import logging
+from threading import Event
+
+from slack_sdk.socket_mode.client import BaseSocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+
+from slack_bolt import App
+from slack_bolt.util.utils import get_boot_message
+
+
+class BaseSocketModeHandler:
+ app: App # type: ignore
+ client: BaseSocketModeClient
+
+ def handle(self, client: BaseSocketModeClient, req: SocketModeRequest) -> None:
+ raise NotImplementedError()
+
+ def connect(self):
+ self.client.connect()
+
+ def disconnect(self):
+ self.client.disconnect()
+
+ def close(self):
+ self.client.close()
+
+ def start(self):
+ self.connect()
+ if self.app.logger.level > logging.INFO:
+ print(get_boot_message())
+ else:
+ self.app.logger.info(get_boot_message())
+ Event().wait()
diff --git a/slack_bolt/adapter/socket_mode/builtin/__init__.py b/slack_bolt/adapter/socket_mode/builtin/__init__.py
new file mode 100644
index 000000000..242a4d5fa
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/builtin/__init__.py
@@ -0,0 +1,56 @@
+import os
+from logging import Logger
+from time import time
+from typing import Optional
+
+from slack_sdk import WebClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.socket_mode.builtin import SocketModeClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode.base_handler import BaseSocketModeHandler
+from slack_bolt.adapter.socket_mode.internals import run_bolt_app, send_response
+from slack_bolt.response import BoltResponse
+
+
+class SocketModeHandler(BaseSocketModeHandler):
+ app: App # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: App, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[WebClient] = None,
+ proxy: Optional[str] = None,
+ auto_reconnect_enabled: bool = True,
+ trace_enabled: bool = False,
+ all_message_trace_enabled: bool = False,
+ ping_pong_trace_enabled: bool = False,
+ ping_interval: float = 10,
+ receive_buffer_size: int = 1024,
+ concurrency: int = 10,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ proxy=proxy if proxy is not None else app.client.proxy,
+ auto_reconnect_enabled=auto_reconnect_enabled,
+ trace_enabled=trace_enabled,
+ all_message_trace_enabled=all_message_trace_enabled,
+ ping_pong_trace_enabled=ping_pong_trace_enabled,
+ ping_interval=ping_interval,
+ receive_buffer_size=receive_buffer_size,
+ concurrency=concurrency,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = run_bolt_app(self.app, req)
+ send_response(client, req, bolt_resp, start)
diff --git a/slack_bolt/adapter/socket_mode/internals.py b/slack_bolt/adapter/socket_mode/internals.py
new file mode 100644
index 000000000..4f7f4d68c
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/internals.py
@@ -0,0 +1,50 @@
+import json
+import logging
+from time import time
+
+from slack_sdk.socket_mode.client import BaseSocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.socket_mode.response import SocketModeResponse
+
+from slack_bolt.app import App
+from slack_bolt.request import BoltRequest
+from slack_bolt.response import BoltResponse
+
+
+def run_bolt_app(app: App, req: SocketModeRequest): # type: ignore
+ bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload)
+ bolt_resp: BoltResponse = app.dispatch(bolt_req)
+ return bolt_resp
+
+
+def send_response(
+ client: BaseSocketModeClient,
+ req: SocketModeRequest,
+ bolt_resp: BoltResponse,
+ start_time: float,
+):
+ if bolt_resp.status == 200:
+ content_type = bolt_resp.headers.get("content-type", [""])[0]
+ if bolt_resp.body is None or len(bolt_resp.body) == 0:
+ client.send_socket_mode_response(
+ SocketModeResponse(envelope_id=req.envelope_id)
+ )
+ elif content_type.startswith("application/json"):
+ dict_body = json.loads(bolt_resp.body)
+ client.send_socket_mode_response(
+ SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)
+ )
+ else:
+ client.send_socket_mode_response(
+ SocketModeResponse(
+ envelope_id=req.envelope_id, payload={"text": bolt_resp.body}
+ )
+ )
+
+ if client.logger.level <= logging.DEBUG:
+ spent_time = int((time() - start_time) * 1000)
+ client.logger.debug(f"Response time: {spent_time} milliseconds")
+ else:
+ client.logger.info(
+ f"Unsuccessful Bolt execution result (status: {bolt_resp.status}, body: {bolt_resp.body})"
+ )
diff --git a/slack_bolt/adapter/socket_mode/websocket_client/__init__.py b/slack_bolt/adapter/socket_mode/websocket_client/__init__.py
new file mode 100644
index 000000000..589edb80d
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/websocket_client/__init__.py
@@ -0,0 +1,54 @@
+import os
+from logging import Logger
+from time import time
+from typing import Optional, Tuple
+
+from slack_sdk import WebClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.socket_mode.websocket_client import SocketModeClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode.base_handler import BaseSocketModeHandler
+from slack_bolt.adapter.socket_mode.internals import run_bolt_app, send_response
+from slack_bolt.response import BoltResponse
+
+
+class SocketModeHandler(BaseSocketModeHandler):
+ app: App # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: App, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[WebClient] = None,
+ ping_interval: float = 10,
+ concurrency: int = 10,
+ http_proxy_host: Optional[str] = None,
+ http_proxy_port: Optional[int] = None,
+ http_proxy_auth: Optional[Tuple[str, str]] = None,
+ proxy_type: Optional[str] = None,
+ trace_enabled: bool = False,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ ping_interval=ping_interval,
+ concurrency=concurrency,
+ http_proxy_host=http_proxy_host,
+ http_proxy_port=http_proxy_port,
+ http_proxy_auth=http_proxy_auth,
+ proxy_type=proxy_type,
+ trace_enabled=trace_enabled,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = run_bolt_app(self.app, req)
+ send_response(client, req, bolt_resp, start)
diff --git a/slack_bolt/adapter/socket_mode/websockets/__init__.py b/slack_bolt/adapter/socket_mode/websockets/__init__.py
new file mode 100644
index 000000000..878eaf5e6
--- /dev/null
+++ b/slack_bolt/adapter/socket_mode/websockets/__init__.py
@@ -0,0 +1,76 @@
+import os
+from logging import Logger
+from time import time
+from typing import Optional
+
+from slack_sdk.socket_mode.websockets import SocketModeClient
+from slack_sdk.socket_mode.request import SocketModeRequest
+from slack_sdk.web.async_client import AsyncWebClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode.async_base_handler import AsyncBaseSocketModeHandler
+from slack_bolt.adapter.socket_mode.async_internals import (
+ send_async_response,
+ run_async_bolt_app,
+)
+from slack_bolt.adapter.socket_mode.internals import run_bolt_app
+from slack_bolt.app.async_app import AsyncApp
+from slack_bolt.response import BoltResponse
+
+
+class SocketModeHandler(AsyncBaseSocketModeHandler):
+ app: App # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: App, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[AsyncWebClient] = None,
+ ping_interval: float = 10,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ ping_interval=ping_interval,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = run_bolt_app(self.app, req)
+ await send_async_response(client, req, bolt_resp, start)
+
+
+class AsyncSocketModeHandler(AsyncBaseSocketModeHandler):
+ app: AsyncApp # type: ignore
+ app_token: str
+ client: SocketModeClient
+
+ def __init__( # type: ignore
+ self,
+ app: AsyncApp, # type: ignore
+ app_token: Optional[str] = None,
+ logger: Optional[Logger] = None,
+ web_client: Optional[AsyncWebClient] = None,
+ ping_interval: float = 10,
+ ):
+ self.app = app
+ self.app_token = app_token or os.environ["SLACK_APP_TOKEN"]
+ self.client = SocketModeClient(
+ app_token=self.app_token,
+ logger=logger if logger is not None else app.logger,
+ web_client=web_client if web_client is not None else app.client,
+ ping_interval=ping_interval,
+ )
+ self.client.socket_mode_request_listeners.append(self.handle)
+
+ async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None:
+ start = time()
+ bolt_resp: BoltResponse = await run_async_bolt_app(self.app, req)
+ await send_async_response(client, req, bolt_resp, start)
diff --git a/slack_bolt/kwargs_injection/async_utils.py b/slack_bolt/kwargs_injection/async_utils.py
index a4b069d72..dae473eea 100644
--- a/slack_bolt/kwargs_injection/async_utils.py
+++ b/slack_bolt/kwargs_injection/async_utils.py
@@ -1,4 +1,5 @@
# pytype: skip-file
+import inspect
import logging
from typing import Callable, Dict, Optional, Any, Sequence
@@ -25,6 +26,7 @@ def build_async_required_kwargs(
request: AsyncBoltRequest,
response: Optional[BoltResponse],
next_func: Callable[[], None] = None,
+ this_func: Optional[Callable] = None,
) -> Dict[str, Any]:
all_available_args = {
"logger": logger,
@@ -73,8 +75,12 @@ def build_async_required_kwargs(
if first_arg_name in {"self", "cls"}:
required_arg_names.pop(0)
elif first_arg_name not in all_available_args.keys():
- logger.warning(warning_skip_uncommon_arg_name(first_arg_name))
- required_arg_names.pop(0)
+ if this_func is None:
+ logger.warning(warning_skip_uncommon_arg_name(first_arg_name))
+ required_arg_names.pop(0)
+ elif inspect.ismethod(this_func):
+ # We are sure that we should skip manipulating this arg
+ required_arg_names.pop(0)
kwargs: Dict[str, Any] = {
k: v for k, v in all_available_args.items() if k in required_arg_names
diff --git a/slack_bolt/kwargs_injection/utils.py b/slack_bolt/kwargs_injection/utils.py
index e10febb59..2685ba342 100644
--- a/slack_bolt/kwargs_injection/utils.py
+++ b/slack_bolt/kwargs_injection/utils.py
@@ -1,4 +1,5 @@
# pytype: skip-file
+import inspect
import logging
from typing import Callable, Dict, Optional, Any, Sequence
@@ -25,6 +26,7 @@ def build_required_kwargs(
request: BoltRequest,
response: Optional[BoltResponse],
next_func: Callable[[], None] = None,
+ this_func: Optional[Callable] = None,
) -> Dict[str, Any]:
all_available_args = {
"logger": logger,
@@ -73,8 +75,12 @@ def build_required_kwargs(
if first_arg_name in {"self", "cls"}:
required_arg_names.pop(0)
elif first_arg_name not in all_available_args.keys():
- logger.warning(warning_skip_uncommon_arg_name(first_arg_name))
- required_arg_names.pop(0)
+ if this_func is None:
+ logger.warning(warning_skip_uncommon_arg_name(first_arg_name))
+ required_arg_names.pop(0)
+ elif inspect.ismethod(this_func):
+ # We are sure that we should skip manipulating this arg
+ required_arg_names.pop(0)
kwargs: Dict[str, Any] = {
k: v for k, v in all_available_args.items() if k in required_arg_names
diff --git a/slack_bolt/lazy_listener/async_internals.py b/slack_bolt/lazy_listener/async_internals.py
index a3e3bfe79..db0d21eb9 100644
--- a/slack_bolt/lazy_listener/async_internals.py
+++ b/slack_bolt/lazy_listener/async_internals.py
@@ -23,6 +23,7 @@ async def request_wired_wrapper() -> None:
required_arg_names=arg_names,
request=request,
response=None,
+ this_func=internal_func,
)
)
except Exception as e:
diff --git a/slack_bolt/lazy_listener/internals.py b/slack_bolt/lazy_listener/internals.py
index f2934ef69..95be376ae 100644
--- a/slack_bolt/lazy_listener/internals.py
+++ b/slack_bolt/lazy_listener/internals.py
@@ -23,6 +23,7 @@ def request_wired_func_wrapper() -> None:
required_arg_names=arg_names,
request=request,
response=None,
+ this_func=func,
)
)
except Exception as e:
diff --git a/slack_bolt/listener/async_listener.py b/slack_bolt/listener/async_listener.py
index 567a51e17..19326fe91 100644
--- a/slack_bolt/listener/async_listener.py
+++ b/slack_bolt/listener/async_listener.py
@@ -117,6 +117,7 @@ async def run_ack_function(
required_arg_names=self.arg_names,
request=request,
response=response,
+ this_func=self.ack_function,
)
)
diff --git a/slack_bolt/listener/custom_listener.py b/slack_bolt/listener/custom_listener.py
index 99f8c353f..b38e80324 100644
--- a/slack_bolt/listener/custom_listener.py
+++ b/slack_bolt/listener/custom_listener.py
@@ -52,5 +52,6 @@ def run_ack_function(
required_arg_names=self.arg_names,
request=request,
response=response,
+ this_func=self.ack_function,
)
)
diff --git a/slack_bolt/listener_matcher/async_builtins.py b/slack_bolt/listener_matcher/async_builtins.py
index 95a7b32e8..f8d05ce52 100644
--- a/slack_bolt/listener_matcher/async_builtins.py
+++ b/slack_bolt/listener_matcher/async_builtins.py
@@ -14,5 +14,6 @@ async def async_matches(self, req: AsyncBoltRequest, resp: BoltResponse) -> bool
required_arg_names=self.arg_names,
request=req,
response=resp,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/listener_matcher/async_listener_matcher.py b/slack_bolt/listener_matcher/async_listener_matcher.py
index 29a107e3e..b21872ed9 100644
--- a/slack_bolt/listener_matcher/async_listener_matcher.py
+++ b/slack_bolt/listener_matcher/async_listener_matcher.py
@@ -45,6 +45,7 @@ async def async_matches(self, req: AsyncBoltRequest, resp: BoltResponse) -> bool
required_arg_names=self.arg_names,
request=req,
response=resp,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/listener_matcher/builtins.py b/slack_bolt/listener_matcher/builtins.py
index 27efa2c24..50b64fea6 100644
--- a/slack_bolt/listener_matcher/builtins.py
+++ b/slack_bolt/listener_matcher/builtins.py
@@ -50,6 +50,7 @@ def matches(self, req: BoltRequest, resp: BoltResponse) -> bool:
required_arg_names=self.arg_names,
request=req,
response=resp,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/listener_matcher/custom_listener_matcher.py b/slack_bolt/listener_matcher/custom_listener_matcher.py
index f96c4da9e..4e07006d4 100644
--- a/slack_bolt/listener_matcher/custom_listener_matcher.py
+++ b/slack_bolt/listener_matcher/custom_listener_matcher.py
@@ -28,5 +28,6 @@ def matches(self, req: BoltRequest, resp: BoltResponse) -> bool:
required_arg_names=self.arg_names,
request=req,
response=resp,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/middleware/async_custom_middleware.py b/slack_bolt/middleware/async_custom_middleware.py
index 220a0723a..be44cf2db 100644
--- a/slack_bolt/middleware/async_custom_middleware.py
+++ b/slack_bolt/middleware/async_custom_middleware.py
@@ -39,6 +39,7 @@ async def async_process(
request=req,
response=resp,
next_func=next,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/middleware/custom_middleware.py b/slack_bolt/middleware/custom_middleware.py
index 016b42ee9..f3e4afff8 100644
--- a/slack_bolt/middleware/custom_middleware.py
+++ b/slack_bolt/middleware/custom_middleware.py
@@ -35,6 +35,7 @@ def process(
request=req,
response=resp,
next_func=next,
+ this_func=self.func,
)
)
diff --git a/slack_bolt/version.py b/slack_bolt/version.py
index 9b102be76..4ab11ba1c 100644
--- a/slack_bolt/version.py
+++ b/slack_bolt/version.py
@@ -1 +1 @@
-__version__ = "1.1.5"
+__version__ = "1.2.0rc2"
diff --git a/tests/adapter_tests/socket_mode/__init__.py b/tests/adapter_tests/socket_mode/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/adapter_tests/socket_mode/mock_socket_mode_server.py b/tests/adapter_tests/socket_mode/mock_socket_mode_server.py
new file mode 100644
index 000000000..2b778910c
--- /dev/null
+++ b/tests/adapter_tests/socket_mode/mock_socket_mode_server.py
@@ -0,0 +1,42 @@
+import logging
+from typing import List
+
+socket_mode_envelopes = [
+ """{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"xxx","action_ts":"1610198080.300836","team":{"id":"T111","domain":"seratch"},"user":{"id":"U111","username":"seratch","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}""",
+ """{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"xxx","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"seratch","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""",
+ """{"envelope_id":"08cfc559-d933-402e-a5c1-79e135afaae4","payload":{"token":"xxx","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"c9b466b5-845c-49c6-a371-57ae44359bf1","type":"message","text":"<@W111>","user":"U111","ts":"1610197986.000300","team":"T111","blocks":[{"type":"rich_text","block_id":"1HBPc","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610197986.000300","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610197986,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U111","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":1,"retry_reason":"timeout"}""",
+]
+
+from flask import Flask
+from flask_sockets import Sockets
+
+
+def start_socket_mode_server(self, port: int):
+ def _start_socket_mode_server():
+ logger = logging.getLogger(__name__)
+ app: Flask = Flask(__name__)
+ sockets: Sockets = Sockets(app)
+
+ envelopes_to_consume: List[str] = list(socket_mode_envelopes)
+
+ @sockets.route("/link")
+ def link(ws):
+ while not ws.closed:
+ message = ws.read_message()
+ if message is not None:
+ if len(envelopes_to_consume) > 0:
+ e = envelopes_to_consume.pop(0)
+ logger.debug(f"Send an envelope: {e}")
+ ws.send(e)
+
+ logger.debug(f"Server received a message: {message}")
+ ws.send(message)
+
+ from gevent import pywsgi
+ from geventwebsocket.handler import WebSocketHandler
+
+ server = pywsgi.WSGIServer(("", port), app, handler_class=WebSocketHandler)
+ self.server = server
+ server.serve_forever(stop_timeout=1)
+
+ return _start_socket_mode_server
diff --git a/tests/adapter_tests/socket_mode/mock_web_api_server.py b/tests/adapter_tests/socket_mode/mock_web_api_server.py
new file mode 100644
index 000000000..ac30148d7
--- /dev/null
+++ b/tests/adapter_tests/socket_mode/mock_web_api_server.py
@@ -0,0 +1,153 @@
+import json
+import logging
+import re
+import threading
+from http import HTTPStatus
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from typing import Type
+from unittest import TestCase
+from urllib.parse import urlparse, parse_qs
+
+
+class MockHandler(SimpleHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+ default_request_version = "HTTP/1.1"
+ logger = logging.getLogger(__name__)
+
+ pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE)
+ pattern_for_package_identifier = re.compile("slackclient/(\\S+)")
+
+ def is_valid_user_agent(self):
+ user_agent = self.headers["User-Agent"]
+ return self.pattern_for_language.search(
+ user_agent
+ ) and self.pattern_for_package_identifier.search(user_agent)
+
+ def is_valid_token(self):
+ if self.path.startswith("oauth"):
+ return True
+ return "Authorization" in self.headers and (
+ str(self.headers["Authorization"]).startswith("Bearer xoxb-")
+ or str(self.headers["Authorization"]).startswith("Bearer xapp-")
+ )
+
+ def set_common_headers(self):
+ self.send_header("content-type", "application/json;charset=utf-8")
+ self.send_header("connection", "close")
+ self.end_headers()
+
+ invalid_auth = {
+ "ok": False,
+ "error": "invalid_auth",
+ }
+
+ not_found = {
+ "ok": False,
+ "error": "test_data_not_found",
+ }
+
+ def _handle(self):
+ try:
+ if self.is_valid_token() and self.is_valid_user_agent():
+ parsed_path = urlparse(self.path)
+
+ len_header = self.headers.get("Content-Length") or 0
+ content_len = int(len_header)
+ post_body = self.rfile.read(content_len)
+ request_body = None
+ if post_body:
+ try:
+ post_body = post_body.decode("utf-8")
+ if post_body.startswith("{"):
+ request_body = json.loads(post_body)
+ else:
+ request_body = {
+ k: v[0] for k, v in parse_qs(post_body).items()
+ }
+ except UnicodeDecodeError:
+ pass
+ else:
+ if parsed_path and parsed_path.query:
+ request_body = {
+ k: v[0] for k, v in parse_qs(parsed_path.query).items()
+ }
+
+ body = {"ok": False, "error": "internal_error"}
+ if self.path == "/auth.test":
+ body = {
+ "ok": True,
+ "url": "https://xyz.slack.com/",
+ "team": "Testing Workspace",
+ "user": "bot-user",
+ "team_id": "T111",
+ "user_id": "W11",
+ "bot_id": "B111",
+ "enterprise_id": "E111",
+ "is_enterprise_install": False,
+ }
+ if self.path == "/apps.connections.open":
+ body = {
+ "ok": True,
+ "url": "ws://localhost:3011/link/?ticket=xxx&app_id=yyy",
+ }
+ if self.path == "/api.test" and request_body:
+ body = {"ok": True, "args": request_body}
+ else:
+ body = self.invalid_auth
+
+ if not body:
+ body = self.not_found
+
+ self.send_response(HTTPStatus.OK)
+ self.set_common_headers()
+ self.wfile.write(json.dumps(body).encode("utf-8"))
+ self.wfile.close()
+
+ except Exception as e:
+ self.logger.error(str(e), exc_info=True)
+ raise
+
+ def do_GET(self):
+ self._handle()
+
+ def do_POST(self):
+ self._handle()
+
+
+class MockServerThread(threading.Thread):
+ def __init__(
+ self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler
+ ):
+ threading.Thread.__init__(self)
+ self.handler = handler
+ self.test = test
+
+ def run(self):
+ self.server = HTTPServer(("localhost", 8888), self.handler)
+ self.test.server_url = "http://localhost:8888"
+ self.test.host, self.test.port = self.server.socket.getsockname()
+ self.test.server_started.set() # threading.Event()
+
+ self.test = None
+ try:
+ self.server.serve_forever()
+ finally:
+ self.server.server_close()
+
+ def stop(self):
+ self.server.shutdown()
+ self.join()
+
+
+def setup_mock_web_api_server(test: TestCase):
+ test.server_started = threading.Event()
+ test.thread = MockServerThread(test)
+ test.thread.start()
+
+ test.server_started.wait()
+
+
+def cleanup_mock_web_api_server(test: TestCase):
+ test.thread.stop()
+
+ test.thread = None
diff --git a/tests/adapter_tests/socket_mode/test_interactions_builtin.py b/tests/adapter_tests/socket_mode/test_interactions_builtin.py
new file mode 100644
index 000000000..cee5e3248
--- /dev/null
+++ b/tests/adapter_tests/socket_mode/test_interactions_builtin.py
@@ -0,0 +1,75 @@
+import logging
+import time
+from threading import Thread
+
+from slack_sdk import WebClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode import SocketModeHandler
+from .mock_socket_mode_server import (
+ start_socket_mode_server,
+)
+from .mock_web_api_server import (
+ setup_mock_web_api_server,
+ cleanup_mock_web_api_server,
+)
+from ...utils import remove_os_env_temporarily, restore_os_env
+
+
+class TestSocketModeBuiltin:
+ logger = logging.getLogger(__name__)
+
+ def setup_method(self):
+ self.old_os_env = remove_os_env_temporarily()
+ setup_mock_web_api_server(self)
+ self.web_client = WebClient(
+ token="xoxb-api_test",
+ base_url="http://localhost:8888",
+ )
+
+ def teardown_method(self):
+ cleanup_mock_web_api_server(self)
+ restore_os_env(self.old_os_env)
+
+ def test_interactions(self):
+ t = Thread(target=start_socket_mode_server(self, 3011))
+ t.daemon = True
+ t.start()
+ time.sleep(2) # wait for the server
+
+ app = App(client=self.web_client)
+
+ result = {"shortcut": False, "command": False}
+
+ @app.shortcut("do-something")
+ def shortcut_handler(ack):
+ result["shortcut"] = True
+ ack()
+
+ @app.command("/hello-socket-mode")
+ def command_handler(ack):
+ result["command"] = True
+ ack()
+
+ handler = SocketModeHandler(
+ app_token="xapp-A111-222-xyz",
+ app=app,
+ trace_enabled=True,
+ )
+ try:
+ handler.client.ping_pong_trace_enabled = True
+ handler.client.wss_uri = "ws://127.0.0.1:3011/link"
+
+ handler.connect()
+ assert handler.client.is_connected() is True
+ time.sleep(2) # wait for the message receiver
+
+ handler.client.send_message("foo")
+
+ time.sleep(2)
+ assert result["shortcut"] is True
+ assert result["command"] is True
+ finally:
+ handler.client.close()
+ self.server.stop()
+ self.server.close()
diff --git a/tests/adapter_tests/socket_mode/test_interactions_web_client.py b/tests/adapter_tests/socket_mode/test_interactions_web_client.py
new file mode 100644
index 000000000..7fd3ca0fc
--- /dev/null
+++ b/tests/adapter_tests/socket_mode/test_interactions_web_client.py
@@ -0,0 +1,74 @@
+import logging
+import time
+from threading import Thread
+
+from slack_sdk import WebClient
+
+from slack_bolt import App
+from slack_bolt.adapter.socket_mode.websocket_client import SocketModeHandler
+from .mock_socket_mode_server import (
+ start_socket_mode_server,
+)
+from .mock_web_api_server import (
+ setup_mock_web_api_server,
+ cleanup_mock_web_api_server,
+)
+from ...utils import remove_os_env_temporarily, restore_os_env
+
+
+class TestSocketModeWebsocketClient:
+ logger = logging.getLogger(__name__)
+
+ def setup_method(self):
+ self.old_os_env = remove_os_env_temporarily()
+ setup_mock_web_api_server(self)
+ self.web_client = WebClient(
+ token="xoxb-api_test",
+ base_url="http://localhost:8888",
+ )
+
+ def teardown_method(self):
+ cleanup_mock_web_api_server(self)
+ restore_os_env(self.old_os_env)
+
+ def test_interactions(self):
+ t = Thread(target=start_socket_mode_server(self, 3012))
+ t.daemon = True
+ t.start()
+ time.sleep(1) # wait for the server
+
+ app = App(client=self.web_client)
+
+ result = {"shortcut": False, "command": False}
+
+ @app.shortcut("do-something")
+ def shortcut_handler(ack):
+ result["shortcut"] = True
+ ack()
+
+ @app.command("/hello-socket-mode")
+ def command_handler(ack):
+ result["command"] = True
+ ack()
+
+ handler = SocketModeHandler(
+ app_token="xapp-A111-222-xyz",
+ app=app,
+ trace_enabled=True,
+ )
+ try:
+ handler.client.wss_uri = "ws://localhost:3012/link"
+
+ handler.connect()
+ assert handler.client.is_connected() is True
+ time.sleep(2) # wait for the message receiver
+
+ handler.client.send_message("foo")
+
+ time.sleep(2)
+ assert result["shortcut"] is True
+ assert result["command"] is True
+ finally:
+ handler.client.close()
+ self.server.stop()
+ self.server.close()
diff --git a/tests/adapter_tests_async/socket_mode/__init__.py b/tests/adapter_tests_async/socket_mode/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/adapter_tests_async/socket_mode/test_async_aiohttp.py b/tests/adapter_tests_async/socket_mode/test_async_aiohttp.py
new file mode 100644
index 000000000..c8eb5b5ab
--- /dev/null
+++ b/tests/adapter_tests_async/socket_mode/test_async_aiohttp.py
@@ -0,0 +1,78 @@
+import asyncio
+from threading import Thread
+
+import pytest
+from slack_sdk.web.async_client import AsyncWebClient
+
+from slack_bolt.adapter.socket_mode.aiohttp import AsyncSocketModeHandler
+from slack_bolt.app.async_app import AsyncApp
+from tests.mock_web_api_server import (
+ setup_mock_web_api_server,
+ cleanup_mock_web_api_server,
+)
+from tests.utils import remove_os_env_temporarily, restore_os_env
+from ...adapter_tests.socket_mode.mock_socket_mode_server import (
+ start_socket_mode_server,
+)
+
+
+class TestSocketModeAiohttp:
+ valid_token = "xoxb-valid"
+ mock_api_server_base_url = "http://localhost:8888"
+ web_client = AsyncWebClient(
+ token=valid_token,
+ base_url=mock_api_server_base_url,
+ )
+
+ @pytest.fixture
+ def event_loop(self):
+ old_os_env = remove_os_env_temporarily()
+ try:
+ setup_mock_web_api_server(self)
+ loop = asyncio.get_event_loop()
+ yield loop
+ loop.close()
+ cleanup_mock_web_api_server(self)
+ finally:
+ restore_os_env(old_os_env)
+
+ @pytest.mark.asyncio
+ async def test_events(self):
+ t = Thread(target=start_socket_mode_server(self, 3021))
+ t.daemon = True
+ t.start()
+ await asyncio.sleep(1) # wait for the server
+
+ app = AsyncApp(client=self.web_client)
+
+ result = {"shortcut": False, "command": False}
+
+ @app.shortcut("do-something")
+ async def shortcut_handler(ack):
+ result["shortcut"] = True
+ await ack()
+
+ @app.command("/hello-socket-mode")
+ async def command_handler(ack):
+ result["command"] = True
+ await ack()
+
+ handler = AsyncSocketModeHandler(
+ app_token="xapp-A111-222-xyz",
+ app=app,
+ )
+ try:
+ handler.client.wss_uri = "ws://localhost:3021/link"
+
+ await handler.connect_async()
+ await asyncio.sleep(2) # wait for the message receiver
+
+ await handler.client.send_message("foo")
+
+ await asyncio.sleep(2)
+ assert result["shortcut"] is True
+ assert result["command"] is True
+ finally:
+ await handler.client.close()
+ self.server.stop()
+ self.server.close()
diff --git a/tests/adapter_tests_async/socket_mode/test_async_websockets.py b/tests/adapter_tests_async/socket_mode/test_async_websockets.py
new file mode 100644
index 000000000..c47d2b7fe
--- /dev/null
+++ b/tests/adapter_tests_async/socket_mode/test_async_websockets.py
@@ -0,0 +1,78 @@
+import asyncio
+from threading import Thread
+
+import pytest
+from slack_sdk.web.async_client import AsyncWebClient
+
+from slack_bolt.adapter.socket_mode.websockets import AsyncSocketModeHandler
+from slack_bolt.app.async_app import AsyncApp
+from tests.mock_web_api_server import (
+ setup_mock_web_api_server,
+ cleanup_mock_web_api_server,
+)
+from tests.utils import remove_os_env_temporarily, restore_os_env
+from ...adapter_tests.socket_mode.mock_socket_mode_server import (
+ start_socket_mode_server,
+)
+
+
+class TestSocketModeWebsockets:
+ valid_token = "xoxb-valid"
+ mock_api_server_base_url = "http://localhost:8888"
+ web_client = AsyncWebClient(
+ token=valid_token,
+ base_url=mock_api_server_base_url,
+ )
+
+ @pytest.fixture
+ def event_loop(self):
+ old_os_env = remove_os_env_temporarily()
+ try:
+ setup_mock_web_api_server(self)
+ loop = asyncio.get_event_loop()
+ yield loop
+ loop.close()
+ cleanup_mock_web_api_server(self)
+ finally:
+ restore_os_env(old_os_env)
+
+ @pytest.mark.asyncio
+ async def test_events(self):
+ t = Thread(target=start_socket_mode_server(self, 3022))
+ t.daemon = True
+ t.start()
+ await asyncio.sleep(1) # wait for the server
+
+ app = AsyncApp(client=self.web_client)
+
+ result = {"shortcut": False, "command": False}
+
+ @app.shortcut("do-something")
+ async def shortcut_handler(ack):
+ result["shortcut"] = True
+ await ack()
+
+ @app.command("/hello-socket-mode")
+ async def command_handler(ack):
+ result["command"] = True
+ await ack()
+
+ handler = AsyncSocketModeHandler(
+ app_token="xapp-A111-222-xyz",
+ app=app,
+ )
+ try:
+ handler.client.wss_uri = "ws://localhost:3022/link"
+
+ await handler.connect_async()
+ await asyncio.sleep(2) # wait for the message receiver
+
+ await handler.client.send_message("foo")
+
+ await asyncio.sleep(2)
+ assert result["shortcut"] is True
+ assert result["command"] is True
+ finally:
+ await handler.client.close()
+ self.server.stop()
+ self.server.close()
diff --git a/tests/scenario_tests/test_app_using_methods_in_class.py b/tests/scenario_tests/test_app_using_methods_in_class.py
index 4102c819c..642d0641d 100644
--- a/tests/scenario_tests/test_app_using_methods_in_class.py
+++ b/tests/scenario_tests/test_app_using_methods_in_class.py
@@ -1,3 +1,4 @@
+import inspect
import json
from time import time, sleep
from typing import Callable
@@ -31,6 +32,29 @@ def teardown_method(self):
cleanup_mock_web_api_server(self)
restore_os_env(self.old_os_env)
+ def test_inspect_behaviors(self):
+ def f():
+ pass
+
+ assert inspect.ismethod(f) is False
+
+ class A:
+ def b(self):
+ pass
+
+ @classmethod
+ def c(cls):
+ pass
+
+ @staticmethod
+ def d():
+ pass
+
+ a = A()
+ assert inspect.ismethod(a.b) is True
+ assert inspect.ismethod(A.c) is True
+ assert inspect.ismethod(A.d) is False
+
def run_app_and_verify(self, app: App):
payload = {
"type": "message_action",
@@ -119,12 +143,24 @@ def test_instance_methods_uncommon_name(self):
app.shortcut("test-shortcut")(awesome.instance_method2)
self.run_app_and_verify(app)
+ def test_instance_methods_uncommon_name_3(self):
+ app = App(client=self.web_client, signing_secret=self.signing_secret)
+ awesome = AwesomeClass("Slackbot")
+ app.use(awesome.instance_middleware)
+ app.shortcut("test-shortcut")(awesome.instance_method3)
+ self.run_app_and_verify(app)
+
def test_static_methods(self):
app = App(client=self.web_client, signing_secret=self.signing_secret)
app.use(AwesomeClass.static_middleware)
app.shortcut("test-shortcut")(AwesomeClass.static_method)
self.run_app_and_verify(app)
+ def test_invalid_arg_in_func(self):
+ app = App(client=self.web_client, signing_secret=self.signing_secret)
+ app.shortcut("test-shortcut")(top_level_function)
+ self.run_app_and_verify(app)
+
class AwesomeClass:
def __init__(self, name: str):
@@ -159,7 +195,20 @@ def instance_method2(whatever, context: BoltContext, say: Say, ack: Ack):
ack()
say(f"Hello <@{context.user_id}>! My name is {whatever.name}")
+ text = "hello world"
+
+ def instance_method3(this, ack, logger, say):
+ ack()
+ logger.debug(this.text)
+ say(f"Hi there!")
+
@staticmethod
def static_method(context: BoltContext, say: Say, ack: Ack):
ack()
say(f"Hello <@{context.user_id}>!")
+
+
+def top_level_function(invalid_arg, ack, say):
+ assert invalid_arg is None
+ ack()
+ say("Hi")
diff --git a/tests/scenario_tests_async/test_app_using_methods_in_class.py b/tests/scenario_tests_async/test_app_using_methods_in_class.py
index a1131fd35..f58be3af5 100644
--- a/tests/scenario_tests_async/test_app_using_methods_in_class.py
+++ b/tests/scenario_tests_async/test_app_using_methods_in_class.py
@@ -1,4 +1,5 @@
import asyncio
+import inspect
import json
from time import time
from typing import Callable
@@ -41,6 +42,29 @@ def event_loop(self):
finally:
restore_os_env(old_os_env)
+ def test_inspect_behaviors(self):
+ async def f():
+ pass
+
+ assert inspect.ismethod(f) is False
+
+ class A:
+ async def b(self):
+ pass
+
+ @classmethod
+ async def c(cls):
+ pass
+
+ @staticmethod
+ async def d():
+ pass
+
+ a = A()
+ assert inspect.ismethod(a.b) is True
+ assert inspect.ismethod(A.c) is True
+ assert inspect.ismethod(A.d) is False
+
async def run_app_and_verify(self, app: AsyncApp):
payload = {
"type": "message_action",
@@ -126,13 +150,21 @@ async def test_instance_methods(self):
await self.run_app_and_verify(app)
@pytest.mark.asyncio
- async def test_instance_methods_uncommon_name(self):
+ async def test_instance_methods_uncommon_name_1(self):
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret)
awesome = AwesomeClass("Slackbot")
app.use(awesome.instance_middleware)
app.shortcut("test-shortcut")(awesome.instance_method2)
await self.run_app_and_verify(app)
+ @pytest.mark.asyncio
+ async def test_instance_methods_uncommon_name_2(self):
+ app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret)
+ awesome = AwesomeClass("Slackbot")
+ app.use(awesome.instance_middleware)
+ app.shortcut("test-shortcut")(awesome.instance_method3)
+ await self.run_app_and_verify(app)
+
@pytest.mark.asyncio
async def test_static_methods(self):
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret)
@@ -140,6 +172,12 @@ async def test_static_methods(self):
app.shortcut("test-shortcut")(AwesomeClass.static_method)
await self.run_app_and_verify(app)
+ @pytest.mark.asyncio
+ async def test_invalid_arg_in_func(self):
+ app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret)
+ app.shortcut("test-shortcut")(top_level_function)
+ await self.run_app_and_verify(app)
+
class AwesomeClass:
def __init__(self, name: str):
@@ -182,7 +220,20 @@ async def instance_method2(
await ack()
await say(f"Hello <@{context.user_id}>! My name is {whatever.name}")
+ text = "hello world"
+
+ async def instance_method3(this, ack, logger, say):
+ await ack()
+ logger.debug(this.text)
+ await say(f"Hi there!")
+
@staticmethod
async def static_method(context: AsyncBoltContext, say: AsyncSay, ack: AsyncAck):
await ack()
await say(f"Hello <@{context.user_id}>!")
+
+
+async def top_level_function(invalid_arg, ack, say):
+ assert invalid_arg is None
+ await ack()
+ await say("Hi")