Skip to content

Commit

Permalink
feat: add custom step support (#1021)
Browse files Browse the repository at this point in the history
* Add remote function support (#986)

* add listener

---------

Co-authored-by: Kazuhiro Sera <[email protected]>

* versions 1.19.0rc1

* Update slack_bolt/app/app.py

Co-authored-by: Kazuhiro Sera <[email protected]>

* Update slack_bolt/app/async_app.py

Co-authored-by: Kazuhiro Sera <[email protected]>

* Update slack_bolt/app/app.py

Co-authored-by: Kazuhiro Sera <[email protected]>

* Improve default values for helper functions

* fix test with new speed updates

* Improve unit test speed

* bump min version of the sdk

* Update README.md

* Update slack_bolt/context/base_context.py

Co-authored-by: Fil Maj <[email protected]>

* Update slack_bolt/context/base_context.py

Co-authored-by: Fil Maj <[email protected]>

* improve based on feedback

* fix linting issue

* Fix typo in readme

* Update README.md

---------

Co-authored-by: Kazuhiro Sera <[email protected]>
Co-authored-by: Fil Maj <[email protected]>
  • Loading branch information
3 people authored Aug 14, 2024
1 parent fbd6462 commit 08be044
Show file tree
Hide file tree
Showing 41 changed files with 1,379 additions and 14 deletions.
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,29 +95,32 @@ Apps typically react to a collection of incoming events, which can correspond to
request, there's a method to build a listener function.

```python
# Listen for an event from the Events API
app.event(event_type)(fn)

# Convenience method to listen to only `message` events using a string or re.Pattern
app.message([pattern ,])(fn)

# Listen for an action from a Block Kit element (buttons, select menus, date pickers, etc)
app.action(action_id)(fn)

# Listen for dialog submissions
app.action({"callback_id": callbackId})(fn)

# Listen for a global or message shortcuts
app.shortcut(callback_id)(fn)

# Listen for slash commands
app.command(command_name)(fn)

# Listen for view_submission modal events
app.view(callback_id)(fn)
# Listen for an event from the Events API
app.event(event_type)(fn)

# Listen for a custom step execution from a workflow
app.function(callback_id)(fn)

# Convenience method to listen to only `message` events using a string or re.Pattern
app.message([pattern ,])(fn)

# Listen for options requests (from select menus with an external data source)
app.options(action_id)(fn)

# Listen for a global or message shortcuts
app.shortcut(callback_id)(fn)

# Listen for view_submission modal events
app.view(callback_id)(fn)
```

The recommended way to use these methods are decorators:
Expand All @@ -142,6 +145,8 @@ Most of the app's functionality will be inside listener functions (the `fn` para
| `say` | Utility function to send a message to the channel associated with the incoming event. This argument is only available when the listener is triggered for events that contain a `channel_id` (the most common being `message` events). `say` accepts simple strings (for plain-text messages) and dictionaries (for messages containing blocks).
| `client` | Web API client that uses the token associated with the event. For single-workspace installations, the token is provided to the constructor. For multi-workspace installations, the token is returned by using [the OAuth library](https://slack.dev/bolt-python/concepts/authenticating-oauth), or manually using the `authorize` function.
| `logger` | The built-in [`logging.Logger`](https://docs.python.org/3/library/logging.html) instance you can use in middleware/listeners.
| `complete` | Utility function used to signal the successful completion of a custom step execution. This tells Slack to proceed with the next steps in the workflow. This argument is only available with the `.function` and `.action` listener when handling custom workflow step executions.
| `fail` | Utility function used to signal that a custom step failed to complete. This tells Slack to stop the workflow execution. This argument is only available with the `.function` and `.action` listener when handling custom workflow step executions.

## Creating an async app

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
slack_sdk>=3.25.0,<4
slack_sdk>=3.26.0,<4
4 changes: 4 additions & 0 deletions slack_bolt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from .app import App
from .context import BoltContext
from .context.ack import Ack
from .context.complete import Complete
from .context.fail import Fail
from .context.respond import Respond
from .context.say import Say
from .kwargs_injection import Args
Expand All @@ -21,6 +23,8 @@
"App",
"BoltContext",
"Ack",
"Complete",
"Fail",
"Respond",
"Say",
"Args",
Expand Down
52 changes: 52 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
MultiTeamsAuthorization,
IgnoringSelfEvents,
CustomMiddleware,
AttachingFunctionToken,
)
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
from slack_bolt.middleware.middleware_error_handler import (
Expand Down Expand Up @@ -113,6 +114,7 @@ def __init__(
ignoring_self_events_enabled: bool = True,
ssl_check_enabled: bool = True,
url_verification_enabled: bool = True,
attaching_function_token_enabled: bool = True,
# for the OAuth flow
oauth_settings: Optional[OAuthSettings] = None,
oauth_flow: Optional[OAuthFlow] = None,
Expand Down Expand Up @@ -178,6 +180,9 @@ def message_hello(message, say):
url_verification_enabled: False if you would like to disable the built-in middleware (Default: True).
`UrlVerification` is a built-in middleware that handles url_verification requests
that verify the endpoint for Events API in HTTP Mode requests.
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
`AttachingFunctionToken` is a built-in middleware that injects the just-in-time workflow-execution tokens
when your app receives `function_executed` or interactivity events scoped to a custom step.
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
`SslCheck` is a built-in middleware that handles ssl_check requests from Slack.
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
Expand Down Expand Up @@ -352,6 +357,7 @@ def message_hello(message, say):
ignoring_self_events_enabled=ignoring_self_events_enabled,
ssl_check_enabled=ssl_check_enabled,
url_verification_enabled=url_verification_enabled,
attaching_function_token_enabled=attaching_function_token_enabled,
user_facing_authorize_error_message=user_facing_authorize_error_message,
)

Expand All @@ -362,6 +368,7 @@ def _init_middleware_list(
ignoring_self_events_enabled: bool = True,
ssl_check_enabled: bool = True,
url_verification_enabled: bool = True,
attaching_function_token_enabled: bool = True,
user_facing_authorize_error_message: Optional[str] = None,
):
if self._init_middleware_list_done:
Expand Down Expand Up @@ -419,6 +426,8 @@ def _init_middleware_list(
self._middleware_list.append(IgnoringSelfEvents(base_logger=self._base_logger))
if url_verification_enabled is True:
self._middleware_list.append(UrlVerification(base_logger=self._base_logger))
if attaching_function_token_enabled is True:
self._middleware_list.append(AttachingFunctionToken())
self._init_middleware_list_done = True

# -------------------------
Expand Down Expand Up @@ -853,6 +862,49 @@ def __call__(*args, **kwargs):

return __call__

def function(
self,
callback_id: Union[str, Pattern],
matchers: Optional[Sequence[Callable[..., bool]]] = None,
middleware: Optional[Sequence[Union[Callable, Middleware]]] = None,
) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]:
"""Registers a new Function listener.
This method can be used as either a decorator or a method.
# Use this method as a decorator
@app.function("reverse")
def reverse_string(ack: Ack, inputs: dict, complete: Complete, fail: Fail):
try:
ack()
string_to_reverse = inputs["stringToReverse"]
complete(outputs={"reverseString": string_to_reverse[::-1]})
except Exception as e:
fail(f"Cannot reverse string (error: {e})")
raise e
# Pass a function to this method
app.function("reverse")(reverse_string)
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.args`'s API document.
Args:
callback_id: The callback id to identify the function
matchers: A list of listener matcher functions.
Only when all the matchers return True, the listener function can be invoked.
middleware: A list of lister middleware functions.
Only when all the middleware call `next()` method, the listener function can be invoked.
"""

matchers = list(matchers) if matchers else []
middleware = list(middleware) if middleware else []

def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.function_executed(callback_id=callback_id, base_logger=self._base_logger)
return self._register_listener(functions, primary_matcher, matchers, middleware, True)

return __call__

# -------------------------
# slash commands

Expand Down
54 changes: 54 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
AsyncRequestVerification,
AsyncIgnoringSelfEvents,
AsyncUrlVerification,
AsyncAttachingFunctionToken,
)
from slack_bolt.middleware.async_custom_middleware import (
AsyncMiddleware,
Expand Down Expand Up @@ -124,6 +125,7 @@ def __init__(
ignoring_self_events_enabled: bool = True,
ssl_check_enabled: bool = True,
url_verification_enabled: bool = True,
attaching_function_token_enabled: bool = True,
# for the OAuth flow
oauth_settings: Optional[AsyncOAuthSettings] = None,
oauth_flow: Optional[AsyncOAuthFlow] = None,
Expand Down Expand Up @@ -188,6 +190,9 @@ async def message_hello(message, say): # async function
that verify the endpoint for Events API in HTTP Mode requests.
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
`AsyncSslCheck` is a built-in middleware that handles ssl_check requests from Slack.
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
`AsyncAttachingFunctionToken` is a built-in middleware that injects the just-in-time workflow-execution token
when your app receives `function_executed` or interactivity events scoped to a custom step.
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
oauth_flow: Instantiated `slack_bolt.oauth.AsyncOAuthFlow`. This is always prioritized over oauth_settings.
verification_token: Deprecated verification mechanism. This can used only for ssl_check requests.
Expand Down Expand Up @@ -358,6 +363,7 @@ async def message_hello(message, say): # async function
ignoring_self_events_enabled=ignoring_self_events_enabled,
ssl_check_enabled=ssl_check_enabled,
url_verification_enabled=url_verification_enabled,
attaching_function_token_enabled=attaching_function_token_enabled,
user_facing_authorize_error_message=user_facing_authorize_error_message,
)

Expand All @@ -369,6 +375,7 @@ def _init_async_middleware_list(
ignoring_self_events_enabled: bool = True,
ssl_check_enabled: bool = True,
url_verification_enabled: bool = True,
attaching_function_token_enabled: bool = True,
user_facing_authorize_error_message: Optional[str] = None,
):
if self._init_middleware_list_done:
Expand Down Expand Up @@ -419,6 +426,8 @@ def _init_async_middleware_list(
self._async_middleware_list.append(AsyncIgnoringSelfEvents(base_logger=self._base_logger))
if url_verification_enabled is True:
self._async_middleware_list.append(AsyncUrlVerification(base_logger=self._base_logger))
if attaching_function_token_enabled is True:
self._async_middleware_list.append(AsyncAttachingFunctionToken())
self._init_middleware_list_done = True

# -------------------------
Expand Down Expand Up @@ -889,6 +898,51 @@ def __call__(*args, **kwargs):

return __call__

def function(
self,
callback_id: Union[str, Pattern],
matchers: Optional[Sequence[Callable[..., Awaitable[bool]]]] = None,
middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None,
) -> Callable[..., Optional[Callable[..., Awaitable[BoltResponse]]]]:
"""Registers a new Function listener.
This method can be used as either a decorator or a method.
# Use this method as a decorator
@app.function("reverse")
async def reverse_string(ack: AsyncAck, inputs: dict, complete: AsyncComplete, fail: AsyncFail):
try:
await ack()
string_to_reverse = inputs["stringToReverse"]
await complete({"reverseString": string_to_reverse[::-1]})
except Exception as e:
await fail(f"Cannot reverse string (error: {e})")
raise e
# Pass a function to this method
app.function("reverse")(reverse_string)
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.async_args`'s API document.
Args:
callback_id: The callback id to identify the function
matchers: A list of listener matcher functions.
Only when all the matchers return True, the listener function can be invoked.
middleware: A list of lister middleware functions.
Only when all the middleware call `next()` method, the listener function can be invoked.
"""

matchers = list(matchers) if matchers else []
middleware = list(middleware) if middleware else []

def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.function_executed(
callback_id=callback_id, base_logger=self._base_logger, asyncio=True
)
return self._register_listener(functions, primary_matcher, matchers, middleware, True)

return __call__

# -------------------------
# slash commands

Expand Down
50 changes: 50 additions & 0 deletions slack_bolt/context/async_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from slack_bolt.context.ack.async_ack import AsyncAck
from slack_bolt.context.base_context import BaseContext
from slack_bolt.context.complete.async_complete import AsyncComplete
from slack_bolt.context.fail.async_fail import AsyncFail
from slack_bolt.context.respond.async_respond import AsyncRespond
from slack_bolt.context.say.async_say import AsyncSay
from slack_bolt.util.utils import create_copy
Expand Down Expand Up @@ -122,3 +124,51 @@ async def handle_button_clicks(ack, respond):
ssl=self.client.ssl,
)
return self["respond"]

@property
def complete(self) -> AsyncComplete:
"""`complete()` function for this request. Once a custom function's state is set to complete,
any outputs the function returns will be passed along to the next step of its housing workflow,
or complete the workflow if the function is the last step in a workflow. Additionally,
any interactivity handlers associated to a function invocation will no longer be invocable.
@app.function("reverse")
async def handle_button_clicks(ack, complete):
await ack()
await complete(outputs={"stringReverse":"olleh"})
@app.function("reverse")
async def handle_button_clicks(context):
await context.ack()
await context.complete(outputs={"stringReverse":"olleh"})
Returns:
Callable `complete()` function
"""
if "complete" not in self:
self["complete"] = AsyncComplete(client=self.client, function_execution_id=self.function_execution_id)
return self["complete"]

@property
def fail(self) -> AsyncFail:
"""`fail()` function for this request. Once a custom function's state is set to error,
its housing workflow will be interrupted and any provided error message will be passed
on to the end user through SlackBot. Additionally, any interactivity handlers associated
to a function invocation will no longer be invocable.
@app.function("reverse")
async def handle_button_clicks(ack, fail):
await ack()
await fail(error="something went wrong")
@app.function("reverse")
async def handle_button_clicks(context):
await context.ack()
await context.fail(error="something went wrong")
Returns:
Callable `fail()` function
"""
if "fail" not in self:
self["fail"] = AsyncFail(client=self.client, function_execution_id=self.function_execution_id)
return self["fail"]
Loading

0 comments on commit 08be044

Please sign in to comment.