Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom step support #1021

Merged
merged 29 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
370cddf
Add remote function support (#986)
WilliamBergamin Jan 25, 2024
1f867bc
versions 1.19.0rc1
WilliamBergamin Jan 25, 2024
5f20da2
Update slack_bolt/app/app.py
WilliamBergamin Jan 26, 2024
1c4325d
Update slack_bolt/app/async_app.py
WilliamBergamin Jan 26, 2024
d742e42
Update slack_bolt/app/app.py
WilliamBergamin Jan 26, 2024
87bb932
Improve default values for helper functions
WilliamBergamin Jan 26, 2024
e523f32
Merge branch 'main' into feat-functions
WilliamBergamin Feb 7, 2024
28663c2
fix test with new speed updates
WilliamBergamin Feb 7, 2024
d33e5c6
Merge branch 'main' into feat-functions
WilliamBergamin Feb 29, 2024
f2294c4
Merge branch 'main' into feat-functions
WilliamBergamin Mar 12, 2024
0204604
Merge branch 'main' into feat-functions
WilliamBergamin Mar 13, 2024
e7e10e8
Merge branch 'main' into feat-functions
WilliamBergamin Apr 11, 2024
9b94f8f
Merge branch 'main' into feat-functions
WilliamBergamin May 10, 2024
053b6c6
Merge branch 'main' into feat-functions
seratch May 28, 2024
fff7c9f
Merge branch 'main' into feat-functions
WilliamBergamin Jun 10, 2024
d858913
Merge branch 'main' into feat-functions
WilliamBergamin Jul 26, 2024
b79a4c3
Improve unit test speed
WilliamBergamin Jul 26, 2024
3ab97e2
bump min version of the sdk
WilliamBergamin Aug 1, 2024
976f43c
Merge branch 'main' into feat-functions
WilliamBergamin Aug 2, 2024
5c26a46
Merge branch 'main' into feat-functions
WilliamBergamin Aug 8, 2024
8ea3a7f
Merge branch 'main' into feat-functions
WilliamBergamin Aug 9, 2024
8c5686e
Update README.md
WilliamBergamin Aug 9, 2024
df12ad9
Merge branch 'main' into feat-functions
WilliamBergamin Aug 12, 2024
d7e231c
Update slack_bolt/context/base_context.py
WilliamBergamin Aug 13, 2024
f6e8aaa
Update slack_bolt/context/base_context.py
WilliamBergamin Aug 13, 2024
a399b11
improve based on feedback
WilliamBergamin Aug 13, 2024
528c131
fix linting issue
WilliamBergamin Aug 13, 2024
78c00c2
Fix typo in readme
WilliamBergamin Aug 13, 2024
cd82e9a
Update README.md
WilliamBergamin Aug 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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