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 2 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
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
53 changes: 53 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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 @@ -111,6 +112,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 @@ -174,6 +176,8 @@ 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 handles tokens with function requests from Slack.
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -348,6 +352,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,
)

def _init_middleware_list(
Expand All @@ -357,6 +362,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,
):
if self._init_middleware_list_done:
return
Expand Down Expand Up @@ -407,6 +413,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 @@ -828,6 +836,51 @@ 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(event, client: WebClient, context: BoltContext):
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
try:
string_to_reverse = event["inputs"]["stringToReverse"]
client.functions_completeSuccess(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this example use the client directly, or the complete / fail shorthand parameter callbacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call 💯 I think complete / fail shorthand parameter are better

This example it good to show how to hand roll completing a function 🤔 not sure where we could include it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just explaining what complete/fail do, and which underlying HTTP API methods they invoke, is sufficient. Link to the relevant methods and developers will figure the rest out.

function_execution_id=context.function_execution_id,
outputs={"reverseString": string_to_reverse[::-1]},
)
except Exception as e:
client.api_call(
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
client.functions_completeError(
function_execution_id=context.function_execution_id,
error=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 @@ -77,6 +77,7 @@
AsyncRequestVerification,
AsyncIgnoringSelfEvents,
AsyncUrlVerification,
AsyncAttachingFunctionToken,
)
from slack_bolt.middleware.async_custom_middleware import (
AsyncMiddleware,
Expand Down Expand Up @@ -122,6 +123,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 @@ -184,6 +186,8 @@ 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 handles tokens with function requests from Slack.
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 @@ -354,6 +358,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,
)

self._server: Optional[AsyncSlackAppServer] = None
Expand All @@ -364,6 +369,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,
):
if self._init_middleware_list_done:
return
Expand Down Expand Up @@ -403,6 +409,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 @@ -861,6 +869,52 @@ 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(event, client: AsyncWebClient, complete: AsyncComplete):
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
try:
string_to_reverse = event["inputs"]["stringToReverse"]
await client.functions_completeSuccess(
function_execution_id=context.function_execution_id,
outputs={"reverseString": string_to_reverse[::-1]},
)
except Exception as e:
await client.functions_completeError(
function_execution_id=context.function_execution_id,
error=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"]
22 changes: 21 additions & 1 deletion slack_bolt/context/base_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Note: Since 2021.12.8, the pytype code analyzer does not properly work for this file

from logging import Logger
from typing import Optional, Tuple
from typing import Any, Dict, Optional, Tuple

from slack_bolt.authorization import AuthorizeResult

Expand All @@ -24,14 +24,19 @@
"response_url",
"matches",
"authorize_result",
"function_bot_access_token",
"bot_token",
"bot_id",
"bot_user_id",
"user_token",
"function_execution_id",
"inputs",
"client",
"ack",
"say",
"respond",
"complete",
"fail",
]

@property
Expand Down Expand Up @@ -103,13 +108,28 @@
"""Returns all the matched parts in message listener's regexp"""
return self.get("matches")

@property
def function_execution_id(self) -> Optional[str]:
"""The `function_execution_id` associated with this request. Only available for function related events"""
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
return self.get("function_execution_id")

@property
def inputs(self) -> Optional[Dict[str, Any]]:
"""The `inputs` associated with this request. Only available for function related events"""
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
return self.get("inputs")

Check warning on line 119 in slack_bolt/context/base_context.py

View check run for this annotation

Codecov / codecov/patch

slack_bolt/context/base_context.py#L119

Added line #L119 was not covered by tests

# --------------------------------

@property
def authorize_result(self) -> Optional[AuthorizeResult]:
"""The authorize result resolved for this request."""
return self.get("authorize_result")

@property
def function_bot_access_token(self) -> Optional[str]:
"""The bot token resolved for this function request. Only available for function related events"""
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
return self.get("function_bot_access_token")

@property
def bot_token(self) -> Optional[str]:
"""The bot token resolved for this request."""
Expand Down
6 changes: 6 additions & 0 deletions slack_bolt/context/complete/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Don't add async module imports here
from .complete import Complete

__all__ = [
"Complete",
]
34 changes: 34 additions & 0 deletions slack_bolt/context/complete/async_complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Any, Dict, Optional

from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.web.async_slack_response import AsyncSlackResponse


class AsyncComplete:
client: AsyncWebClient
function_execution_id: Optional[str]

def __init__(
self,
client: AsyncWebClient,
function_execution_id: Optional[str],
):
self.client = client
self.function_execution_id = function_execution_id

async def __call__(self, outputs: Dict[str, Any] = {}) -> AsyncSlackResponse:
"""Signal the successful completion of the custom function.

Kwargs:
outputs: Json serializable object containing the output values

Returns:
SlackResponse: The response object returned from slack

Raises:
ValueError: If this function cannot be used.
"""
if self.function_execution_id is None:
raise ValueError("complete is unsupported here as there is no function_execution_id")

return await self.client.functions_completeSuccess(function_execution_id=self.function_execution_id, outputs=outputs)
Loading
Loading