Skip to content

Commit 370cddf

Browse files
Add remote function support (#986)
* add listener --------- Co-authored-by: Kazuhiro Sera <[email protected]>
1 parent 4b086cf commit 370cddf

39 files changed

+1358
-4
lines changed

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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
MultiTeamsAuthorization,
6363
IgnoringSelfEvents,
6464
CustomMiddleware,
65+
AttachingFunctionToken,
6566
)
6667
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
6768
from slack_bolt.middleware.middleware_error_handler import (
@@ -111,6 +112,7 @@ def __init__(
111112
ignoring_self_events_enabled: bool = True,
112113
ssl_check_enabled: bool = True,
113114
url_verification_enabled: bool = True,
115+
attaching_function_token_enabled: bool = True,
114116
# for the OAuth flow
115117
oauth_settings: Optional[OAuthSettings] = None,
116118
oauth_flow: Optional[OAuthFlow] = None,
@@ -174,6 +176,8 @@ def message_hello(message, say):
174176
url_verification_enabled: False if you would like to disable the built-in middleware (Default: True).
175177
`UrlVerification` is a built-in middleware that handles url_verification requests
176178
that verify the endpoint for Events API in HTTP Mode requests.
179+
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
180+
`AttachingFunctionToken` is a built-in middleware that handles tokens with function requests from Slack.
177181
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
178182
`SslCheck` is a built-in middleware that handles ssl_check requests from Slack.
179183
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
@@ -348,6 +352,7 @@ def message_hello(message, say):
348352
ignoring_self_events_enabled=ignoring_self_events_enabled,
349353
ssl_check_enabled=ssl_check_enabled,
350354
url_verification_enabled=url_verification_enabled,
355+
attaching_function_token_enabled=attaching_function_token_enabled,
351356
)
352357

353358
def _init_middleware_list(
@@ -357,6 +362,7 @@ def _init_middleware_list(
357362
ignoring_self_events_enabled: bool = True,
358363
ssl_check_enabled: bool = True,
359364
url_verification_enabled: bool = True,
365+
attaching_function_token_enabled: bool = True,
360366
):
361367
if self._init_middleware_list_done:
362368
return
@@ -407,6 +413,8 @@ def _init_middleware_list(
407413
self._middleware_list.append(IgnoringSelfEvents(base_logger=self._base_logger))
408414
if url_verification_enabled is True:
409415
self._middleware_list.append(UrlVerification(base_logger=self._base_logger))
416+
if attaching_function_token_enabled is True:
417+
self._middleware_list.append(AttachingFunctionToken())
410418
self._init_middleware_list_done = True
411419

412420
# -------------------------
@@ -828,6 +836,51 @@ def __call__(*args, **kwargs):
828836

829837
return __call__
830838

839+
def function(
840+
self,
841+
callback_id: Union[str, Pattern],
842+
matchers: Optional[Sequence[Callable[..., bool]]] = None,
843+
middleware: Optional[Sequence[Union[Callable, Middleware]]] = None,
844+
) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]:
845+
"""Registers a new Function listener.
846+
This method can be used as either a decorator or a method.
847+
# Use this method as a decorator
848+
@app.function("reverse")
849+
def reverse_string(event, client: WebClient, context: BoltContext):
850+
try:
851+
string_to_reverse = event["inputs"]["stringToReverse"]
852+
client.functions_completeSuccess(
853+
function_execution_id=context.function_execution_id,
854+
outputs={"reverseString": string_to_reverse[::-1]},
855+
)
856+
except Exception as e:
857+
client.api_call(
858+
client.functions_completeError(
859+
function_execution_id=context.function_execution_id,
860+
error=f"Cannot reverse string (error: {e})",
861+
)
862+
raise e
863+
# Pass a function to this method
864+
app.function("reverse")(reverse_string)
865+
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.args`'s API document.
866+
Args:
867+
callback_id: The callback id to identify the function
868+
matchers: A list of listener matcher functions.
869+
Only when all the matchers return True, the listener function can be invoked.
870+
middleware: A list of lister middleware functions.
871+
Only when all the middleware call `next()` method, the listener function can be invoked.
872+
"""
873+
874+
matchers = list(matchers) if matchers else []
875+
middleware = list(middleware) if middleware else []
876+
877+
def __call__(*args, **kwargs):
878+
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
879+
primary_matcher = builtin_matchers.function_executed(callback_id=callback_id, base_logger=self._base_logger)
880+
return self._register_listener(functions, primary_matcher, matchers, middleware, True)
881+
882+
return __call__
883+
831884
# -------------------------
832885
# slash commands
833886

slack_bolt/app/async_app.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
AsyncRequestVerification,
7878
AsyncIgnoringSelfEvents,
7979
AsyncUrlVerification,
80+
AsyncAttachingFunctionToken,
8081
)
8182
from slack_bolt.middleware.async_custom_middleware import (
8283
AsyncMiddleware,
@@ -122,6 +123,7 @@ def __init__(
122123
ignoring_self_events_enabled: bool = True,
123124
ssl_check_enabled: bool = True,
124125
url_verification_enabled: bool = True,
126+
attaching_function_token_enabled: bool = True,
125127
# for the OAuth flow
126128
oauth_settings: Optional[AsyncOAuthSettings] = None,
127129
oauth_flow: Optional[AsyncOAuthFlow] = None,
@@ -184,6 +186,8 @@ async def message_hello(message, say): # async function
184186
that verify the endpoint for Events API in HTTP Mode requests.
185187
ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True).
186188
`AsyncSslCheck` is a built-in middleware that handles ssl_check requests from Slack.
189+
attaching_function_token_enabled: False if you would like to disable the built-in middleware (Default: True).
190+
`AsyncAttachingFunctionToken` is a built-in middleware that handles tokens with function requests from Slack.
187191
oauth_settings: The settings related to Slack app installation flow (OAuth flow)
188192
oauth_flow: Instantiated `slack_bolt.oauth.AsyncOAuthFlow`. This is always prioritized over oauth_settings.
189193
verification_token: Deprecated verification mechanism. This can used only for ssl_check requests.
@@ -354,6 +358,7 @@ async def message_hello(message, say): # async function
354358
ignoring_self_events_enabled=ignoring_self_events_enabled,
355359
ssl_check_enabled=ssl_check_enabled,
356360
url_verification_enabled=url_verification_enabled,
361+
attaching_function_token_enabled=attaching_function_token_enabled,
357362
)
358363

359364
self._server: Optional[AsyncSlackAppServer] = None
@@ -364,6 +369,7 @@ def _init_async_middleware_list(
364369
ignoring_self_events_enabled: bool = True,
365370
ssl_check_enabled: bool = True,
366371
url_verification_enabled: bool = True,
372+
attaching_function_token_enabled: bool = True,
367373
):
368374
if self._init_middleware_list_done:
369375
return
@@ -403,6 +409,8 @@ def _init_async_middleware_list(
403409
self._async_middleware_list.append(AsyncIgnoringSelfEvents(base_logger=self._base_logger))
404410
if url_verification_enabled is True:
405411
self._async_middleware_list.append(AsyncUrlVerification(base_logger=self._base_logger))
412+
if attaching_function_token_enabled is True:
413+
self._async_middleware_list.append(AsyncAttachingFunctionToken())
406414
self._init_middleware_list_done = True
407415

408416
# -------------------------
@@ -861,6 +869,52 @@ def __call__(*args, **kwargs):
861869

862870
return __call__
863871

872+
def function(
873+
self,
874+
callback_id: Union[str, Pattern],
875+
matchers: Optional[Sequence[Callable[..., Awaitable[bool]]]] = None,
876+
middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None,
877+
) -> Callable[..., Optional[Callable[..., Awaitable[BoltResponse]]]]:
878+
"""Registers a new Function listener.
879+
This method can be used as either a decorator or a method.
880+
# Use this method as a decorator
881+
@app.function("reverse")
882+
async def reverse_string(event, client: AsyncWebClient, complete: AsyncComplete):
883+
try:
884+
string_to_reverse = event["inputs"]["stringToReverse"]
885+
await client.functions_completeSuccess(
886+
function_execution_id=context.function_execution_id,
887+
outputs={"reverseString": string_to_reverse[::-1]},
888+
)
889+
except Exception as e:
890+
await client.functions_completeError(
891+
function_execution_id=context.function_execution_id,
892+
error=f"Cannot reverse string (error: {e})",
893+
)
894+
raise e
895+
# Pass a function to this method
896+
app.function("reverse")(reverse_string)
897+
To learn available arguments for middleware/listeners, see `slack_bolt.kwargs_injection.async_args`'s API document.
898+
Args:
899+
callback_id: The callback id to identify the function
900+
matchers: A list of listener matcher functions.
901+
Only when all the matchers return True, the listener function can be invoked.
902+
middleware: A list of lister middleware functions.
903+
Only when all the middleware call `next()` method, the listener function can be invoked.
904+
"""
905+
906+
matchers = list(matchers) if matchers else []
907+
middleware = list(middleware) if middleware else []
908+
909+
def __call__(*args, **kwargs):
910+
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
911+
primary_matcher = builtin_matchers.function_executed(
912+
callback_id=callback_id, base_logger=self._base_logger, asyncio=True
913+
)
914+
return self._register_listener(functions, primary_matcher, matchers, middleware, True)
915+
916+
return __call__
917+
864918
# -------------------------
865919
# slash commands
866920

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"]

slack_bolt/context/base_context.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Note: Since 2021.12.8, the pytype code analyzer does not properly work for this file
33

44
from logging import Logger
5-
from typing import Optional, Tuple
5+
from typing import Any, Dict, Optional, Tuple
66

77
from slack_bolt.authorization import AuthorizeResult
88

@@ -24,14 +24,19 @@ class BaseContext(dict):
2424
"response_url",
2525
"matches",
2626
"authorize_result",
27+
"function_bot_access_token",
2728
"bot_token",
2829
"bot_id",
2930
"bot_user_id",
3031
"user_token",
32+
"function_execution_id",
33+
"inputs",
3134
"client",
3235
"ack",
3336
"say",
3437
"respond",
38+
"complete",
39+
"fail",
3540
]
3641

3742
@property
@@ -103,13 +108,28 @@ def matches(self) -> Optional[Tuple]:
103108
"""Returns all the matched parts in message listener's regexp"""
104109
return self.get("matches")
105110

111+
@property
112+
def function_execution_id(self) -> Optional[str]:
113+
"""The `function_execution_id` associated with this request. Only available for function related events"""
114+
return self.get("function_execution_id")
115+
116+
@property
117+
def inputs(self) -> Optional[Dict[str, Any]]:
118+
"""The `inputs` associated with this request. Only available for function related events"""
119+
return self.get("inputs")
120+
106121
# --------------------------------
107122

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

128+
@property
129+
def function_bot_access_token(self) -> Optional[str]:
130+
"""The bot token resolved for this function request. Only available for function related events"""
131+
return self.get("function_bot_access_token")
132+
113133
@property
114134
def bot_token(self) -> Optional[str]:
115135
"""The bot token resolved for this request."""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Don't add async module imports here
2+
from .complete import Complete
3+
4+
__all__ = [
5+
"Complete",
6+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Any, Dict, Optional
2+
3+
from slack_sdk.web.async_client import AsyncWebClient
4+
from slack_sdk.web.async_slack_response import AsyncSlackResponse
5+
6+
7+
class AsyncComplete:
8+
client: AsyncWebClient
9+
function_execution_id: Optional[str]
10+
11+
def __init__(
12+
self,
13+
client: AsyncWebClient,
14+
function_execution_id: Optional[str],
15+
):
16+
self.client = client
17+
self.function_execution_id = function_execution_id
18+
19+
async def __call__(self, outputs: Dict[str, Any] = {}) -> AsyncSlackResponse:
20+
"""Signal the successful completion of the custom function.
21+
22+
Kwargs:
23+
outputs: Json serializable object containing the output values
24+
25+
Returns:
26+
SlackResponse: The response object returned from slack
27+
28+
Raises:
29+
ValueError: If this function cannot be used.
30+
"""
31+
if self.function_execution_id is None:
32+
raise ValueError("complete is unsupported here as there is no function_execution_id")
33+
34+
return await self.client.functions_completeSuccess(function_execution_id=self.function_execution_id, outputs=outputs)

0 commit comments

Comments
 (0)