Skip to content

Commit

Permalink
Add remote function support (#986)
Browse files Browse the repository at this point in the history
* add listener

---------

Co-authored-by: Kazuhiro Sera <[email protected]>
  • Loading branch information
WilliamBergamin and seratch authored Jan 25, 2024
1 parent 4b086cf commit 370cddf
Show file tree
Hide file tree
Showing 39 changed files with 1,358 additions and 4 deletions.
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.
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):
try:
string_to_reverse = event["inputs"]["stringToReverse"]
client.functions_completeSuccess(
function_execution_id=context.function_execution_id,
outputs={"reverseString": string_to_reverse[::-1]},
)
except Exception as e:
client.api_call(
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):
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 @@ class BaseContext(dict):
"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 @@ def matches(self) -> Optional[Tuple]:
"""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"""
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"""
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"""
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

0 comments on commit 370cddf

Please sign in to comment.