Skip to content

Commit 08be044

Browse files
WilliamBergaminseratchfilmaj
authored
feat: add custom step support (#1021)
* 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]>
1 parent fbd6462 commit 08be044

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1379
-14
lines changed

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,29 +95,32 @@ Apps typically react to a collection of incoming events, which can correspond to
9595
request, there's a method to build a listener function.
9696

9797
```python
98-
# Listen for an event from the Events API
99-
app.event(event_type)(fn)
100-
101-
# Convenience method to listen to only `message` events using a string or re.Pattern
102-
app.message([pattern ,])(fn)
103-
10498
# Listen for an action from a Block Kit element (buttons, select menus, date pickers, etc)
10599
app.action(action_id)(fn)
106100

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

110-
# Listen for a global or message shortcuts
111-
app.shortcut(callback_id)(fn)
112-
113104
# Listen for slash commands
114105
app.command(command_name)(fn)
115106

116-
# Listen for view_submission modal events
117-
app.view(callback_id)(fn)
107+
# Listen for an event from the Events API
108+
app.event(event_type)(fn)
109+
110+
# Listen for a custom step execution from a workflow
111+
app.function(callback_id)(fn)
112+
113+
# Convenience method to listen to only `message` events using a string or re.Pattern
114+
app.message([pattern ,])(fn)
118115

119116
# Listen for options requests (from select menus with an external data source)
120117
app.options(action_id)(fn)
118+
119+
# Listen for a global or message shortcuts
120+
app.shortcut(callback_id)(fn)
121+
122+
# Listen for view_submission modal events
123+
app.view(callback_id)(fn)
121124
```
122125

123126
The recommended way to use these methods are decorators:
@@ -142,6 +145,8 @@ Most of the app's functionality will be inside listener functions (the `fn` para
142145
| `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).
143146
| `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.
144147
| `logger` | The built-in [`logging.Logger`](https://docs.python.org/3/library/logging.html) instance you can use in middleware/listeners.
148+
| `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.
149+
| `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.
145150

146151
## Creating an async app
147152

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
slack_sdk>=3.25.0,<4
1+
slack_sdk>=3.26.0,<4

slack_bolt/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from .app import App
1010
from .context import BoltContext
1111
from .context.ack import Ack
12+
from .context.complete import Complete
13+
from .context.fail import Fail
1214
from .context.respond import Respond
1315
from .context.say import Say
1416
from .kwargs_injection import Args
@@ -21,6 +23,8 @@
2123
"App",
2224
"BoltContext",
2325
"Ack",
26+
"Complete",
27+
"Fail",
2428
"Respond",
2529
"Say",
2630
"Args",

slack_bolt/app/app.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
MultiTeamsAuthorization,
6464
IgnoringSelfEvents,
6565
CustomMiddleware,
66+
AttachingFunctionToken,
6667
)
6768
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
6869
from slack_bolt.middleware.middleware_error_handler import (
@@ -113,6 +114,7 @@ def __init__(
113114
ignoring_self_events_enabled: bool = True,
114115
ssl_check_enabled: bool = True,
115116
url_verification_enabled: bool = True,
117+
attaching_function_token_enabled: bool = True,
116118
# for the OAuth flow
117119
oauth_settings: Optional[OAuthSettings] = None,
118120
oauth_flow: Optional[OAuthFlow] = None,
@@ -178,6 +180,9 @@ def message_hello(message, say):
178180
url_verification_enabled: False if you would like to disable the built-in middleware (Default: True).
179181
`UrlVerification` is a built-in middleware that handles url_verification requests
180182
that verify the endpoint for Events API in HTTP Mode requests.
183+
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
184+
`AttachingFunctionToken` is a built-in middleware that injects the just-in-time workflow-execution tokens
185+
when your app receives `function_executed` or interactivity events scoped to a custom step.
181186
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
182187
`SslCheck` is a built-in middleware that handles ssl_check requests from Slack.
183188
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
@@ -352,6 +357,7 @@ def message_hello(message, say):
352357
ignoring_self_events_enabled=ignoring_self_events_enabled,
353358
ssl_check_enabled=ssl_check_enabled,
354359
url_verification_enabled=url_verification_enabled,
360+
attaching_function_token_enabled=attaching_function_token_enabled,
355361
user_facing_authorize_error_message=user_facing_authorize_error_message,
356362
)
357363

@@ -362,6 +368,7 @@ def _init_middleware_list(
362368
ignoring_self_events_enabled: bool = True,
363369
ssl_check_enabled: bool = True,
364370
url_verification_enabled: bool = True,
371+
attaching_function_token_enabled: bool = True,
365372
user_facing_authorize_error_message: Optional[str] = None,
366373
):
367374
if self._init_middleware_list_done:
@@ -419,6 +426,8 @@ def _init_middleware_list(
419426
self._middleware_list.append(IgnoringSelfEvents(base_logger=self._base_logger))
420427
if url_verification_enabled is True:
421428
self._middleware_list.append(UrlVerification(base_logger=self._base_logger))
429+
if attaching_function_token_enabled is True:
430+
self._middleware_list.append(AttachingFunctionToken())
422431
self._init_middleware_list_done = True
423432

424433
# -------------------------
@@ -853,6 +862,49 @@ def __call__(*args, **kwargs):
853862

854863
return __call__
855864

865+
def function(
866+
self,
867+
callback_id: Union[str, Pattern],
868+
matchers: Optional[Sequence[Callable[..., bool]]] = None,
869+
middleware: Optional[Sequence[Union[Callable, Middleware]]] = None,
870+
) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]:
871+
"""Registers a new Function listener.
872+
This method can be used as either a decorator or a method.
873+
874+
# Use this method as a decorator
875+
@app.function("reverse")
876+
def reverse_string(ack: Ack, inputs: dict, complete: Complete, fail: Fail):
877+
try:
878+
ack()
879+
string_to_reverse = inputs["stringToReverse"]
880+
complete(outputs={"reverseString": string_to_reverse[::-1]})
881+
except Exception as e:
882+
fail(f"Cannot reverse string (error: {e})")
883+
raise e
884+
885+
# Pass a function to this method
886+
app.function("reverse")(reverse_string)
887+
888+
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.args`'s API document.
889+
890+
Args:
891+
callback_id: The callback id to identify the function
892+
matchers: A list of listener matcher functions.
893+
Only when all the matchers return True, the listener function can be invoked.
894+
middleware: A list of lister middleware functions.
895+
Only when all the middleware call `next()` method, the listener function can be invoked.
896+
"""
897+
898+
matchers = list(matchers) if matchers else []
899+
middleware = list(middleware) if middleware else []
900+
901+
def __call__(*args, **kwargs):
902+
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
903+
primary_matcher = builtin_matchers.function_executed(callback_id=callback_id, base_logger=self._base_logger)
904+
return self._register_listener(functions, primary_matcher, matchers, middleware, True)
905+
906+
return __call__
907+
856908
# -------------------------
857909
# slash commands
858910

slack_bolt/app/async_app.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
AsyncRequestVerification,
7979
AsyncIgnoringSelfEvents,
8080
AsyncUrlVerification,
81+
AsyncAttachingFunctionToken,
8182
)
8283
from slack_bolt.middleware.async_custom_middleware import (
8384
AsyncMiddleware,
@@ -124,6 +125,7 @@ def __init__(
124125
ignoring_self_events_enabled: bool = True,
125126
ssl_check_enabled: bool = True,
126127
url_verification_enabled: bool = True,
128+
attaching_function_token_enabled: bool = True,
127129
# for the OAuth flow
128130
oauth_settings: Optional[AsyncOAuthSettings] = None,
129131
oauth_flow: Optional[AsyncOAuthFlow] = None,
@@ -188,6 +190,9 @@ async def message_hello(message, say): # async function
188190
that verify the endpoint for Events API in HTTP Mode requests.
189191
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
190192
`AsyncSslCheck` is a built-in middleware that handles ssl_check requests from Slack.
193+
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
194+
`AsyncAttachingFunctionToken` is a built-in middleware that injects the just-in-time workflow-execution token
195+
when your app receives `function_executed` or interactivity events scoped to a custom step.
191196
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
192197
oauth_flow: Instantiated `slack_bolt.oauth.AsyncOAuthFlow`. This is always prioritized over oauth_settings.
193198
verification_token: Deprecated verification mechanism. This can used only for ssl_check requests.
@@ -358,6 +363,7 @@ async def message_hello(message, say): # async function
358363
ignoring_self_events_enabled=ignoring_self_events_enabled,
359364
ssl_check_enabled=ssl_check_enabled,
360365
url_verification_enabled=url_verification_enabled,
366+
attaching_function_token_enabled=attaching_function_token_enabled,
361367
user_facing_authorize_error_message=user_facing_authorize_error_message,
362368
)
363369

@@ -369,6 +375,7 @@ def _init_async_middleware_list(
369375
ignoring_self_events_enabled: bool = True,
370376
ssl_check_enabled: bool = True,
371377
url_verification_enabled: bool = True,
378+
attaching_function_token_enabled: bool = True,
372379
user_facing_authorize_error_message: Optional[str] = None,
373380
):
374381
if self._init_middleware_list_done:
@@ -419,6 +426,8 @@ def _init_async_middleware_list(
419426
self._async_middleware_list.append(AsyncIgnoringSelfEvents(base_logger=self._base_logger))
420427
if url_verification_enabled is True:
421428
self._async_middleware_list.append(AsyncUrlVerification(base_logger=self._base_logger))
429+
if attaching_function_token_enabled is True:
430+
self._async_middleware_list.append(AsyncAttachingFunctionToken())
422431
self._init_middleware_list_done = True
423432

424433
# -------------------------
@@ -889,6 +898,51 @@ def __call__(*args, **kwargs):
889898

890899
return __call__
891900

901+
def function(
902+
self,
903+
callback_id: Union[str, Pattern],
904+
matchers: Optional[Sequence[Callable[..., Awaitable[bool]]]] = None,
905+
middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None,
906+
) -> Callable[..., Optional[Callable[..., Awaitable[BoltResponse]]]]:
907+
"""Registers a new Function listener.
908+
This method can be used as either a decorator or a method.
909+
910+
# Use this method as a decorator
911+
@app.function("reverse")
912+
async def reverse_string(ack: AsyncAck, inputs: dict, complete: AsyncComplete, fail: AsyncFail):
913+
try:
914+
await ack()
915+
string_to_reverse = inputs["stringToReverse"]
916+
await complete({"reverseString": string_to_reverse[::-1]})
917+
except Exception as e:
918+
await fail(f"Cannot reverse string (error: {e})")
919+
raise e
920+
921+
# Pass a function to this method
922+
app.function("reverse")(reverse_string)
923+
924+
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.async_args`'s API document.
925+
926+
Args:
927+
callback_id: The callback id to identify the function
928+
matchers: A list of listener matcher functions.
929+
Only when all the matchers return True, the listener function can be invoked.
930+
middleware: A list of lister middleware functions.
931+
Only when all the middleware call `next()` method, the listener function can be invoked.
932+
"""
933+
934+
matchers = list(matchers) if matchers else []
935+
middleware = list(middleware) if middleware else []
936+
937+
def __call__(*args, **kwargs):
938+
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
939+
primary_matcher = builtin_matchers.function_executed(
940+
callback_id=callback_id, base_logger=self._base_logger, asyncio=True
941+
)
942+
return self._register_listener(functions, primary_matcher, matchers, middleware, True)
943+
944+
return __call__
945+
892946
# -------------------------
893947
# slash commands
894948

slack_bolt/context/async_context.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from slack_bolt.context.ack.async_ack import AsyncAck
66
from slack_bolt.context.base_context import BaseContext
7+
from slack_bolt.context.complete.async_complete import AsyncComplete
8+
from slack_bolt.context.fail.async_fail import AsyncFail
79
from slack_bolt.context.respond.async_respond import AsyncRespond
810
from slack_bolt.context.say.async_say import AsyncSay
911
from slack_bolt.util.utils import create_copy
@@ -122,3 +124,51 @@ async def handle_button_clicks(ack, respond):
122124
ssl=self.client.ssl,
123125
)
124126
return self["respond"]
127+
128+
@property
129+
def complete(self) -> AsyncComplete:
130+
"""`complete()` function for this request. Once a custom function's state is set to complete,
131+
any outputs the function returns will be passed along to the next step of its housing workflow,
132+
or complete the workflow if the function is the last step in a workflow. Additionally,
133+
any interactivity handlers associated to a function invocation will no longer be invocable.
134+
135+
@app.function("reverse")
136+
async def handle_button_clicks(ack, complete):
137+
await ack()
138+
await complete(outputs={"stringReverse":"olleh"})
139+
140+
@app.function("reverse")
141+
async def handle_button_clicks(context):
142+
await context.ack()
143+
await context.complete(outputs={"stringReverse":"olleh"})
144+
145+
Returns:
146+
Callable `complete()` function
147+
"""
148+
if "complete" not in self:
149+
self["complete"] = AsyncComplete(client=self.client, function_execution_id=self.function_execution_id)
150+
return self["complete"]
151+
152+
@property
153+
def fail(self) -> AsyncFail:
154+
"""`fail()` function for this request. Once a custom function's state is set to error,
155+
its housing workflow will be interrupted and any provided error message will be passed
156+
on to the end user through SlackBot. Additionally, any interactivity handlers associated
157+
to a function invocation will no longer be invocable.
158+
159+
@app.function("reverse")
160+
async def handle_button_clicks(ack, fail):
161+
await ack()
162+
await fail(error="something went wrong")
163+
164+
@app.function("reverse")
165+
async def handle_button_clicks(context):
166+
await context.ack()
167+
await context.fail(error="something went wrong")
168+
169+
Returns:
170+
Callable `fail()` function
171+
"""
172+
if "fail" not in self:
173+
self["fail"] = AsyncFail(client=self.client, function_execution_id=self.function_execution_id)
174+
return self["fail"]

0 commit comments

Comments
 (0)