Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4594489
Added cli hooks
WilliamBergamin Jul 20, 2022
9d33ef9
Python module run
WilliamBergamin Jul 25, 2022
3cd8c04
Added console script for get hooks
WilliamBergamin Jul 26, 2022
34d75ba
end of day commit
WilliamBergamin Jul 26, 2022
c0db994
fire alarm
WilliamBergamin Jul 27, 2022
1ea4fc7
Added function capabilities
WilliamBergamin Jul 27, 2022
24ebd45
Added function error response
WilliamBergamin Jul 28, 2022
a23820b
Added callback blacklist
WilliamBergamin Jul 28, 2022
0943f78
Fixed small typo
WilliamBergamin Jul 28, 2022
446678a
Update app.py
WilliamBergamin Jul 28, 2022
944ba29
Renamed args to follow new standard
WilliamBergamin Jul 29, 2022
cc89218
Added some tests
WilliamBergamin Aug 2, 2022
7042914
Added more tests
WilliamBergamin Aug 2, 2022
c670a9a
Added cli test
WilliamBergamin Aug 2, 2022
d91fd02
Update get_manifest.py
WilliamBergamin Aug 2, 2022
e5dcf44
end of day commit
WilliamBergamin Aug 2, 2022
3f32d31
Added functions tests
WilliamBergamin Aug 3, 2022
d318c22
Added get manifest tests
WilliamBergamin Aug 3, 2022
f64928f
Cleaned up code
WilliamBergamin Aug 3, 2022
9eb3cbe
Added tests for the cli
WilliamBergamin Aug 4, 2022
30937d4
end of day commit
WilliamBergamin Aug 4, 2022
f38c6cc
cleaned up code
WilliamBergamin Aug 5, 2022
4e216d6
Fixed flake8 issue
WilliamBergamin Aug 5, 2022
d44c950
Added changes based on feedback
WilliamBergamin Aug 8, 2022
3c53f21
Added changes based on feedback
WilliamBergamin Aug 9, 2022
fbd83df
Had to trim text to comply with linter
WilliamBergamin Aug 9, 2022
c63551d
Fix mistake
WilliamBergamin Aug 9, 2022
b2a3dff
Impoved CLI and manifest search
WilliamBergamin Aug 10, 2022
9754f62
Added async and some unit tests
WilliamBergamin Aug 11, 2022
d274c13
Merge branch 'future' of https://github.com/WilliamBergamin/bolt-pyth…
WilliamBergamin Aug 11, 2022
6042d2a
removed code duplication
WilliamBergamin Aug 11, 2022
d49abb7
Improved based on feedback
WilliamBergamin Aug 19, 2022
7f0593f
dirty prototype
WilliamBergamin Aug 23, 2022
9a49f10
Clean up prototype
WilliamBergamin Aug 23, 2022
e8910aa
Cleaned up prototype
WilliamBergamin Aug 24, 2022
b8d871a
Improved function handler usage
WilliamBergamin Aug 25, 2022
d268819
Handle function token
WilliamBergamin Aug 26, 2022
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
13 changes: 8 additions & 5 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CallableAuthorize,
)
from slack_bolt.error import BoltError, BoltUnhandledRequestError
from slack_bolt.function import Function
from slack_bolt.lazy_listener.thread_runner import ThreadLazyListenerRunner
from slack_bolt.listener.builtins import TokenRevocationListeners
from slack_bolt.listener.custom_listener import CustomListener
Expand Down Expand Up @@ -63,6 +64,7 @@
IgnoringSelfEvents,
CustomMiddleware,
)
from slack_bolt.middleware.function_listener_matches import FunctionListenerToken
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
from slack_bolt.middleware.middleware_error_handler import (
DefaultMiddlewareErrorHandler,
Expand All @@ -78,6 +80,7 @@
create_web_client,
get_boot_message,
get_name_for_callable,
create_copy
)
from slack_bolt.workflows.step import WorkflowStep, WorkflowStepMiddleware
from slack_bolt.workflows.step.step import WorkflowStepBuilder
Expand Down Expand Up @@ -794,7 +797,7 @@ def __call__(*args, **kwargs):

def function(
self,
callback_id: Union[str, Pattern],
callback_id: Union[str, Function],
matchers: Optional[Sequence[Callable[..., bool]]] = None,
middleware: Optional[Sequence[Union[Callable, Middleware]]] = None,
) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]:
Expand Down Expand Up @@ -825,11 +828,11 @@ def reverse_string(event, complete_success: CompleteSuccess, complete_error: Com
middleware: A list of lister middleware functions.
Only when all the middleware call `next()` method, the listener function can be invoked.
"""

middleware = list(middleware) if middleware else []
middleware.insert(0, FunctionListenerToken())
def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.function_event(callback_id=callback_id, base_logger=self._base_logger)
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)
return Function(self._register_listener, self._base_logger, list(functions), callback_id, matchers, middleware)

return __call__

Expand Down Expand Up @@ -1249,7 +1252,7 @@ def _init_context(self, req: BoltRequest):
req.context["token"] = self._token
if self._token is not None:
# This WebClient instance can be safely singleton
req.context["client"] = self._client
req.context["client"] = create_copy(self._client)
else:
# Set a new dedicated instance for this request
client_per_request: WebClient = WebClient(
Expand Down
43 changes: 43 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,49 @@ 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, complete_success: AsyncCompleteSuccess, complete_error: AsyncCompleteError):
try:
string_to_reverse = event["inputs"]["stringToReverse"]
await complete_success({
"reverseString": string_to_reverse[::-1]
})
except Exception as e:
await complete_error("Cannot reverse string")
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.
"""

def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.function_event(
callback_id=callback_id, asyncio=True, base_logger=self._base_logger
)
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)

return __call__

# -------------------------
# slash commands

Expand Down
4 changes: 4 additions & 0 deletions slack_bolt/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ async def command(ack, body, respond):
from .context.async_context import AsyncBoltContext
from .context.respond.async_respond import AsyncRespond
from .context.say.async_say import AsyncSay
from .context.complete_error.async_complete_error import AsyncCompleteError
from .context.complete_success.async_complete_success import AsyncCompleteSuccess
from .listener.async_listener import AsyncListener
from .listener_matcher.async_listener_matcher import AsyncCustomListenerMatcher
from .request.async_request import AsyncBoltRequest
Expand All @@ -59,6 +61,8 @@ async def command(ack, body, respond):
"AsyncBoltContext",
"AsyncRespond",
"AsyncSay",
"AsyncCompleteError",
"AsyncCompleteSuccess",
"AsyncListener",
"AsyncCustomListenerMatcher",
"AsyncBoltRequest",
Expand Down
127 changes: 88 additions & 39 deletions slack_bolt/context/async_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
from slack_bolt.context.base_context import BaseContext
from slack_bolt.context.respond.async_respond import AsyncRespond
from slack_bolt.context.say.async_say import AsyncSay
from slack_bolt.context.complete_error.async_complete_error import AsyncCompleteError
from slack_bolt.context.complete_success.async_complete_success import AsyncCompleteSuccess
from slack_bolt.util.utils import create_copy

CLIENT: str = "client"
ACK: str = "ack"
SAY: str = "say"
RESPOND: str = "respond"
COMPLETE_ERROR: str = "complete_error"
COMPLETE_SUCCESS: str = "complete_success"


class AsyncBoltContext(BaseContext):
"""Context object associated with a request from Slack."""
Expand All @@ -31,94 +40,134 @@ def to_copyable(self) -> "AsyncBoltContext":

@property
def client(self) -> Optional[AsyncWebClient]:
"""The `AsyncWebClient` instance available for this request.
f"""The `AsyncWebClient` instance available for this request.

@app.event("app_mention")
async def handle_events(context):
await context.client.chat_postMessage(
await context.{CLIENT}.chat_postMessage(
channel=context.channel_id,
text="Thanks!",
)

# You can access "client" this way too.
# You can access "{CLIENT}" this way too.
@app.event("app_mention")
async def handle_events(client, context):
await client.chat_postMessage(
async def handle_events({CLIENT}, context):
await {CLIENT}.chat_postMessage(
channel=context.channel_id,
text="Thanks!",
)

Returns:
`AsyncWebClient` instance
"""
if "client" not in self:
self["client"] = AsyncWebClient(token=None)
return self["client"]
if CLIENT not in self:
self[CLIENT] = AsyncWebClient(token=None)
return self[CLIENT]

@property
def ack(self) -> AsyncAck:
"""`ack()` function for this request.
f"""`{ACK}()` function for this request.

@app.action("button")
async def handle_button_clicks(context):
await context.ack()
await context.{ACK}()

# You can access "ack" this way too.
# You can access "{ACK}" this way too.
@app.action("button")
async def handle_button_clicks(ack):
await ack()
async def handle_button_clicks({ACK}):
await {ACK}()

Returns:
Callable `ack()` function
Callable `{ACK}()` function
"""
if "ack" not in self:
self["ack"] = AsyncAck()
return self["ack"]
if ACK not in self:
self[ACK] = AsyncAck()
return self[ACK]

@property
def say(self) -> AsyncSay:
"""`say()` function for this request.
f"""`{SAY}()` function for this request.

@app.action("button")
async def handle_button_clicks(context):
await context.ack()
await context.say("Hi!")
await context.{ACK}()
await context.{SAY}("Hi!")

# You can access "ack" this way too.
# You can access "{ACK}" this way too.
@app.action("button")
async def handle_button_clicks(ack, say):
await ack()
await say("Hi!")
async def handle_button_clicks({ACK}, {SAY}):
await {ACK}()
await {SAY}("Hi!")

Returns:
Callable `say()` function
Callable `{SAY}()` function
"""
if "say" not in self:
self["say"] = AsyncSay(client=self.client, channel=self.channel_id)
return self["say"]
if SAY not in self:
self[SAY] = AsyncSay(client=self.client, channel=self.channel_id)
return self[SAY]

@property
def respond(self) -> Optional[AsyncRespond]:
"""`respond()` function for this request.
f"""`{RESPOND}()` function for this request.

@app.action("button")
async def handle_button_clicks(context):
await context.ack()
await context.respond("Hi!")
await context.{ACK}()
await context.{RESPOND}("Hi!")

# You can access "ack" this way too.
# You can access "{ACK}" this way too.
@app.action("button")
async def handle_button_clicks(ack, respond):
await ack()
await respond("Hi!")
async def handle_button_clicks({ACK}, {RESPOND}):
await {ACK}()
await {RESPOND}("Hi!")

Returns:
Callable `respond()` function
Callable `{RESPOND}()` function
"""
if "respond" not in self:
self["respond"] = AsyncRespond(
if RESPOND not in self:
self[RESPOND] = AsyncRespond(
response_url=self.response_url,
proxy=self.client.proxy,
ssl=self.client.ssl,
)
return self["respond"]
return self[RESPOND]

@property
def complete_success(self) -> AsyncCompleteSuccess:
f"""`{COMPLETE_SUCCESS}()` function for this request.

@app.function("reverse")
async def handle_button_clicks(context):
await context.{COMPLETE_SUCCESS}({{"stringReverse":"olleh"}})

@app.function("reverse")
async def handle_button_clicks({COMPLETE_SUCCESS}):
await {COMPLETE_SUCCESS}({{"stringReverse":"olleh"}})

Returns:
Callable `{COMPLETE_SUCCESS}()` function
"""
if COMPLETE_SUCCESS not in self:
self[COMPLETE_SUCCESS] = AsyncCompleteSuccess(
client=self.client, function_execution_id=self.function_execution_id
)
return self[COMPLETE_SUCCESS]

@property
def complete_error(self) -> AsyncCompleteError:
f"""`{COMPLETE_ERROR}()` function for this request.

@app.function("reverse")
async def handle_button_clicks(context):
await context.{COMPLETE_ERROR}("an error spawned")

@app.function("reverse")
async def handle_button_clicks({COMPLETE_ERROR}):
await {COMPLETE_ERROR}("an error spawned")

Returns:
Callable `{COMPLETE_ERROR}()` function
"""
if COMPLETE_ERROR not in self:
self[COMPLETE_ERROR] = AsyncCompleteError(client=self.client, function_execution_id=self.function_execution_id)
return self[COMPLETE_ERROR]
6 changes: 6 additions & 0 deletions slack_bolt/context/base_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class BaseContext(dict):
"response_url",
"matches",
"authorize_result",
"bot_access_token",
"bot_token",
"bot_id",
"bot_user_id",
Expand Down Expand Up @@ -91,6 +92,11 @@ def authorize_result(self) -> Optional[AuthorizeResult]:
"""The authorize result resolved for this request."""
return self.get("authorize_result")

@property
def bot_access_token(self) -> Optional[str]:
"""The bot token resolved for this function request."""
return self.get("bot_access_token")

@property
def bot_token(self) -> Optional[str]:
"""The bot token resolved for this request."""
Expand Down
32 changes: 32 additions & 0 deletions slack_bolt/context/complete_error/async_complete_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Optional

from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.web.async_slack_response import AsyncSlackResponse
from slack_bolt.context.complete_error.internals import _can_complete


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

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

async def __call__(
self,
message: str,
) -> AsyncSlackResponse:
if _can_complete(self):
if isinstance(message, str):
# TODO add this new api call to the sdk and use it here
return await self.client.api_call(
"functions.completeError", json={"error": message, "function_execution_id": self.function_execution_id}
)
raise ValueError(f"The message arg is unexpected type ({type(message)}) expecting str")
else:
raise ValueError("complete_error is unsupported here as there is no function_execution_id")
6 changes: 2 additions & 4 deletions slack_bolt/context/complete_error/complete_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from slack_sdk import WebClient
from slack_sdk.web import SlackResponse
from slack_bolt.context.complete_error.internals import _can_complete


class CompleteError:
Expand All @@ -20,7 +21,7 @@ def __call__(
self,
message: str,
) -> SlackResponse:
if self._can_complete():
if _can_complete(self):
if isinstance(message, str):
# TODO add this new api call to the sdk and use it here
return self.client.api_call(
Expand All @@ -29,6 +30,3 @@ def __call__(
raise ValueError(f"The message arg is unexpected type ({type(message)}) expecting str")
else:
raise ValueError("complete_error is unsupported here as there is no function_execution_id")

def _can_complete(self) -> bool:
return hasattr(self, "client") and self.client is not None and self.function_execution_id is not None
5 changes: 5 additions & 0 deletions slack_bolt/context/complete_error/internals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Any


def _can_complete(self: Any) -> bool:
return hasattr(self, "client") and self.client is not None and self.function_execution_id is not None
Loading