Ejemplo n.º 1
0
    def test_installation_store_conflicts(self):
        store1 = FileInstallationStore()
        store2 = FileInstallationStore()
        app = AsyncApp(
            signing_secret="valid",
            oauth_settings=AsyncOAuthSettings(
                client_id="111.222", client_secret="valid", installation_store=store1,
            ),
            installation_store=store2,
        )
        assert app.installation_store is store1

        app = AsyncApp(
            signing_secret="valid",
            oauth_flow=AsyncOAuthFlow(
                settings=AsyncOAuthSettings(
                    client_id="111.222",
                    client_secret="valid",
                    installation_store=store1,
                )
            ),
            installation_store=store2,
        )
        assert app.installation_store is store1

        app = AsyncApp(
            signing_secret="valid",
            oauth_flow=AsyncOAuthFlow(
                settings=AsyncOAuthSettings(client_id="111.222", client_secret="valid",)
            ),
            installation_store=store1,
        )
        assert app.installation_store is store1
Ejemplo n.º 2
0
    def __init__(
        self,
        *,
        client: Optional[AsyncWebClient] = None,
        logger: Optional[Logger] = None,
        settings: AsyncOAuthSettings,
    ):
        """The module to run the Slack app installation flow (OAuth flow).

        :param client: The AsyncWebClient.
        :param logger: The logger.
        :param settings: OAuth settings to configure this module.
        """
        self._async_client = client
        self._logger = logger
        self.settings = settings
        self.settings.logger = self._logger

        self.client_id = self.settings.client_id
        self.redirect_uri = self.settings.redirect_uri
        self.install_path = self.settings.install_path
        self.redirect_uri_path = self.settings.redirect_uri_path

        self.default_callback_options = DefaultAsyncCallbackOptions(
            logger=logger,
            state_utils=self.settings.state_utils,
            redirect_uri_page_renderer=self.settings.
            redirect_uri_page_renderer,
        )
        if settings.callback_options is None:
            settings.callback_options = self.default_callback_options
        self.success_handler = settings.callback_options.success
        self.failure_handler = settings.callback_options.failure
Ejemplo n.º 3
0
    def test_oauth(self):
        app = AsyncApp(
            client=self.web_client,
            signing_secret=self.signing_secret,
            oauth_settings=AsyncOAuthSettings(
                client_id="111.111",
                client_secret="xxx",
                scopes=["chat:write", "commands"],
            ),
        )
        app_handler = AsyncSlackRequestHandler(app)

        async def endpoint(req: Request):
            return await app_handler.handle(req)

        api = Starlette(
            debug=True,
            routes=[
                Route("/slack/install", endpoint=endpoint, methods=["GET"])
            ],
        )

        client = TestClient(api)
        response = client.get("/slack/install", allow_redirects=False)
        assert response.status_code == 200
        assert response.headers.get(
            "content-type") == "text/html; charset=utf-8"
        assert response.headers.get("content-length") == "565"
        assert "https://slack.com/oauth/v2/authorize?state=" in response.text
Ejemplo n.º 4
0
    def test_authorize_conflicts(self):
        oauth_settings = AsyncOAuthSettings(
            client_id="111.222",
            client_secret="valid",
            installation_store=FileInstallationStore(),
            state_store=FileOAuthStateStore(expiration_seconds=120),
        )

        # no error with this
        AsyncApp(signing_secret="valid", oauth_settings=oauth_settings)

        def authorize() -> AuthorizeResult:
            return AuthorizeResult(enterprise_id="E111", team_id="T111")

        with pytest.raises(BoltError):
            AsyncApp(
                signing_secret="valid",
                authorize=authorize,
                oauth_settings=oauth_settings,
            )

        oauth_flow = AsyncOAuthFlow(settings=oauth_settings)
        # no error with this
        AsyncApp(signing_secret="valid", oauth_flow=oauth_flow)

        with pytest.raises(BoltError):
            AsyncApp(signing_secret="valid", authorize=authorize, oauth_flow=oauth_flow)
    async def test_handle_callback(self):
        oauth_flow = AsyncOAuthFlow(
            client=AsyncWebClient(base_url=self.mock_api_server_base_url),
            settings=AsyncOAuthSettings(
                client_id="111.222",
                client_secret="xxx",
                scopes=["chat:write", "commands"],
                installation_store=FileInstallationStore(),
                state_store=FileOAuthStateStore(expiration_seconds=120),
                success_url="https://www.example.com/completion",
                failure_url="https://www.example.com/failure",
            ),
        )
        state = await oauth_flow.issue_new_state(None)
        req = AsyncBoltRequest(
            body="",
            query=f"code=foo&state={state}",
            headers={
                "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"]
            },
        )
        resp = await oauth_flow.handle_callback(req)
        assert resp.status == 200
        assert "https://www.example.com/completion" in resp.body

        app = AsyncApp(signing_secret="signing_secret", oauth_flow=oauth_flow)
        global_shortcut_body = {
            "type": "shortcut",
            "token": "verification_token",
            "action_ts": "111.111",
            "team": {
                "id": "T111",
                "domain": "workspace-domain",
                "enterprise_id": "E111",
                "enterprise_name": "Org Name",
            },
            "user": {
                "id": "W111",
                "username": "******",
                "team_id": "T111"
            },
            "callback_id": "test-shortcut",
            "trigger_id": "111.111.xxxxxx",
        }
        body = f"payload={quote(json.dumps(global_shortcut_body))}"
        timestamp = str(int(time()))
        signature_verifier = SignatureVerifier("signing_secret")
        headers = {
            "content-type": ["application/x-www-form-urlencoded"],
            "x-slack-signature": [
                signature_verifier.generate_signature(body=body,
                                                      timestamp=timestamp)
            ],
            "x-slack-request-timestamp": [timestamp],
        }
        request = AsyncBoltRequest(body=body, headers=headers)
        response = await app.async_dispatch(request)
        assert response.status == 200
        await assert_auth_test_count_async(self, 1)
Ejemplo n.º 6
0
 def test_valid_multi_auth(self):
     app = AsyncApp(
         signing_secret="valid",
         oauth_settings=AsyncOAuthSettings(
             client_id="111.222", client_secret="valid"
         ),
     )
     assert app != None
Ejemplo n.º 7
0
 def test_valid_multi_auth_secret_absence(self):
     with pytest.raises(BoltError):
         AsyncApp(
             signing_secret="valid",
             oauth_settings=AsyncOAuthSettings(
                 client_id="111.222", client_secret=None
             ),
         )
 async def test_scopes_as_str(self):
     settings = AsyncOAuthSettings(
         client_id="111.222",
         client_secret="xxx",
         scopes="chat:write,commands",
         user_scopes="search:read",
     )
     assert settings.scopes == ["chat:write", "commands"]
     assert settings.user_scopes == ["search:read"]
Ejemplo n.º 9
0
 def test_valid_multi_auth_oauth_flow(self):
     oauth_flow = AsyncOAuthFlow(
         settings=AsyncOAuthSettings(
             client_id="111.222",
             client_secret="valid",
             installation_store=FileInstallationStore(),
             state_store=FileOAuthStateStore(expiration_seconds=120),
         )
     )
     app = AsyncApp(signing_secret="valid", oauth_flow=oauth_flow)
     assert app != None
 async def test_instantiation(self):
     oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings(
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
         user_scopes=["search:read"],
         installation_store=FileInstallationStore(),
         state_store=FileOAuthStateStore(expiration_seconds=120),
     ))
     assert oauth_flow is not None
     assert oauth_flow.logger is not None
     assert oauth_flow.client is not None
Ejemplo n.º 11
0
 async def test_handle_installation_default(self):
     oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings(
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
         installation_store=FileInstallationStore(),
         state_store=FileOAuthStateStore(expiration_seconds=120),
     ))
     req = AsyncBoltRequest(body="")
     resp = await oauth_flow.handle_installation(req)
     assert resp.status == 200
     assert resp.headers.get("content-type") == ["text/html; charset=utf-8"]
     assert "https://slack.com/oauth/v2/authorize?state=" in resp.body
 async def test_handle_installation_no_rendering(self):
     oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings(
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
         installation_store=FileInstallationStore(),
         install_page_rendering_enabled=False,  # disabled
         state_store=FileOAuthStateStore(expiration_seconds=120),
     ))
     req = AsyncBoltRequest(body="")
     resp = await oauth_flow.handle_installation(req)
     assert resp.status == 302
     location_header = resp.headers.get("location")[0]
     assert "https://slack.com/oauth/v2/authorize?state=" in location_header
    async def test_handle_callback_using_options(self):
        async def success(args: AsyncSuccessArgs) -> BoltResponse:
            assert args.request is not None
            return BoltResponse(status=200, body="customized")

        async def failure(args: AsyncFailureArgs) -> BoltResponse:
            assert args.request is not None
            assert args.reason is not None
            return BoltResponse(status=502, body="customized")

        oauth_flow = AsyncOAuthFlow(
            client=AsyncWebClient(base_url=self.mock_api_server_base_url),
            settings=AsyncOAuthSettings(
                client_id="111.222",
                client_secret="xxx",
                scopes=["chat:write", "commands"],
                installation_store=FileInstallationStore(),
                state_store=FileOAuthStateStore(expiration_seconds=120),
                callback_options=AsyncCallbackOptions(
                    success=success,
                    failure=failure,
                ),
            ),
        )
        state = await oauth_flow.issue_new_state(None)
        req = AsyncBoltRequest(
            body="",
            query=f"code=foo&state={state}",
            headers={
                "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"]
            },
        )
        resp = await oauth_flow.handle_callback(req)
        assert resp.status == 200
        assert resp.body == "customized"

        req = AsyncBoltRequest(
            body="",
            query=f"code=foo&state=invalid",
            headers={
                "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"]
            },
        )
        resp = await oauth_flow.handle_callback(req)
        assert resp.status == 502
        assert resp.body == "customized"
 async def test_handle_callback_invalid_state(self):
     oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings(
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
         installation_store=FileInstallationStore(),
         state_store=FileOAuthStateStore(expiration_seconds=120),
     ))
     state = await oauth_flow.issue_new_state(None)
     req = AsyncBoltRequest(
         body="",
         query=f"code=foo&state=invalid",
         headers={
             "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"]
         },
     )
     resp = await oauth_flow.handle_callback(req)
     assert resp.status == 400
Ejemplo n.º 15
0
    def __init__(
        self,
        *,
        client: Optional[AsyncWebClient] = None,
        logger: Optional[Logger] = None,
        settings: AsyncOAuthSettings,
    ):
        """The module to run the Slack app installation flow (OAuth flow).

        Args:
            client: The `slack_sdk.web.async_client.AsyncWebClient` instance.
            logger: The logger.
            settings: OAuth settings to configure this module.
        """
        self._async_client = client
        self._logger = logger

        if not isinstance(settings, AsyncOAuthSettings):
            raise BoltError(error_oauth_settings_invalid_type_async())
        self.settings = settings

        self.settings.logger = self._logger

        self.client_id = self.settings.client_id
        self.redirect_uri = self.settings.redirect_uri
        self.install_path = self.settings.install_path
        self.redirect_uri_path = self.settings.redirect_uri_path

        self.default_callback_options = DefaultAsyncCallbackOptions(
            logger=logger,
            state_utils=self.settings.state_utils,
            redirect_uri_page_renderer=self.settings.
            redirect_uri_page_renderer,
        )
        if settings.callback_options is None:
            settings.callback_options = self.default_callback_options
        self.success_handler = settings.callback_options.success
        self.failure_handler = settings.callback_options.failure
Ejemplo n.º 16
0
    async def test_oauth(self):
        app = AsyncApp(
            client=self.web_client,
            signing_secret=self.signing_secret,
            oauth_settings=AsyncOAuthSettings(
                client_id="111.111",
                client_secret="xxx",
                scopes=["chat:write", "commands"],
            ),
        )
        api = Sanic(name=self.unique_sanic_app_name())
        app_handler = AsyncSlackRequestHandler(app)

        @api.get("/slack/install")
        async def endpoint(req: Request):
            return await app_handler.handle(req)

        _, response = await api.asgi_client.get(
            url="/slack/install", allow_redirects=False
        )
        assert response.status_code == 200
        assert response.headers.get("content-type") == "text/html; charset=utf-8"
        assert response.headers.get("content-length") == "565"
        assert "https://slack.com/oauth/v2/authorize?state=" in response.text
Ejemplo n.º 17
0
# ------------------

import logging

logging.basicConfig(level=logging.DEBUG)

import os

from slack_bolt.app.async_app import AsyncApp
from slack_bolt.context.async_context import AsyncBoltContext
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings

app = AsyncApp(signing_secret=os.environ["SLACK_SIGNING_SECRET"],
               oauth_settings=AsyncOAuthSettings(
                   client_id=os.environ["SLACK_CLIENT_ID"],
                   client_secret=os.environ["SLACK_CLIENT_SECRET"],
                   scopes=os.environ["SLACK_SCOPES"].split(","),
               ))


@app.event("app_mention")
async def mention(context: AsyncBoltContext):
    await context.say(":wave: Hi there!")


@app.event("message")
async def message(context: AsyncBoltContext, event: dict):
    await context.client.reactions_add(
        channel=event["channel"],
        timestamp=event["ts"],
        name="eyes",
Ejemplo n.º 18
0
            user_scopes=["search:read"],
            installed_at=datetime.datetime.now().timestamp(),
        )


class BotOnlyMemoryInstallationStore(LegacyMemoryInstallationStore):
    async def async_find_installation(
        self,
        *,
        enterprise_id: Optional[str],
        team_id: Optional[str],
        user_id: Optional[str] = None,
        is_enterprise_install: Optional[bool] = False,
    ) -> Optional[Installation]:
        raise ValueError


oauth_settings = AsyncOAuthSettings(
    client_id="111.222",
    client_secret="secret",
    installation_store=BotOnlyMemoryInstallationStore(),
    installation_store_bot_only=False,
)

oauth_settings_bot_only = AsyncOAuthSettings(
    client_id="111.222",
    client_secret="secret",
    installation_store=BotOnlyMemoryInstallationStore(),
    installation_store_bot_only=True,
)
Ejemplo n.º 19
0
    def sqlite3(
        cls,
        database: str,
        # OAuth flow parameters/credentials
        authorization_url: Optional[str] = None,
        client_id: Optional[str] = None,  # required
        client_secret: Optional[str] = None,  # required
        scopes: Optional[Sequence[str]] = None,
        user_scopes: Optional[Sequence[str]] = None,
        redirect_uri: Optional[str] = None,
        # Handler configuration
        install_path: Optional[str] = None,
        redirect_uri_path: Optional[str] = None,
        callback_options: Optional[AsyncCallbackOptions] = None,
        success_url: Optional[str] = None,
        failure_url: Optional[str] = None,
        # Installation Management
        # state parameter related configurations
        state_cookie_name: str = OAuthStateUtils.default_cookie_name,
        state_expiration_seconds: int = OAuthStateUtils.
        default_expiration_seconds,
        client: Optional[AsyncWebClient] = None,
        logger: Optional[Logger] = None,
    ) -> "AsyncOAuthFlow":

        client_id = client_id or os.environ["SLACK_CLIENT_ID"]  # required
        client_secret = client_secret or os.environ[
            "SLACK_CLIENT_SECRET"]  # required
        scopes = scopes or os.environ.get("SLACK_SCOPES", "").split(",")
        user_scopes = user_scopes or os.environ.get("SLACK_USER_SCOPES",
                                                    "").split(",")
        redirect_uri = redirect_uri or os.environ.get("SLACK_REDIRECT_URI")
        return AsyncOAuthFlow(
            client=client or AsyncWebClient(),
            logger=logger,
            settings=AsyncOAuthSettings(
                # OAuth flow parameters/credentials
                authorization_url=authorization_url,
                client_id=client_id,
                client_secret=client_secret,
                scopes=scopes,
                user_scopes=user_scopes,
                redirect_uri=redirect_uri,
                # Handler configuration
                install_path=install_path,
                redirect_uri_path=redirect_uri_path,
                callback_options=callback_options,
                success_url=success_url,
                failure_url=failure_url,
                # Installation Management
                installation_store=SQLite3InstallationStore(
                    database=database,
                    client_id=client_id,
                    logger=logger,
                ),
                # state parameter related configurations
                state_store=SQLite3OAuthStateStore(
                    database=database,
                    expiration_seconds=state_expiration_seconds,
                    logger=logger,
                ),
                state_cookie_name=state_cookie_name,
                state_expiration_seconds=state_expiration_seconds,
            ),
        )
Ejemplo n.º 20
0
import sys
import logging
# Import the async app instead of the regular one
from slack_bolt.async_app import AsyncApp
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore
import carmille

logging.basicConfig(level=logging.INFO)

oauth_settings = AsyncOAuthSettings(
    client_id=os.environ["SLACK_CLIENT_ID"],
    client_secret=os.environ["SLACK_CLIENT_SECRET"],
    scopes=[
        "channels:history", "channels:read", "commands", "emoji:read",
        "reactions:read", "users:read"
    ],
    installation_store=FileInstallationStore(base_dir="./data"),
    state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="./data"))

app = AsyncApp(signing_secret=os.environ["SLACK_SIGNING_SECRET"],
               oauth_settings=oauth_settings)


@app.command("/carmille")
async def command(context, ack, body, respond):
    await ack()

    user_tz_offset = await carmille.fetch.get_tz_offset(
        context.client, body['user_id'])
Ejemplo n.º 21
0
    def __init__(
        self,
        *,
        logger: Optional[logging.Logger] = None,
        # Used in logger
        name: Optional[str] = None,
        # Set True when you run this app on a FaaS platform
        process_before_response: bool = False,
        # Basic Information > Credentials > Signing Secret
        signing_secret: Optional[str] = None,
        # for single-workspace apps
        token: Optional[str] = None,
        client: Optional[AsyncWebClient] = None,
        # for multi-workspace apps
        installation_store: Optional[AsyncInstallationStore] = None,
        authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None,
        # for the OAuth flow
        oauth_settings: Optional[AsyncOAuthSettings] = None,
        oauth_flow: Optional[AsyncOAuthFlow] = None,
        # No need to set (the value is used only in response to ssl_check requests)
        verification_token: Optional[str] = None,
    ):
        """Bolt App that provides functionalities to register middleware/listeners

        :param name: The application name that will be used in logging.
            If absent, the source file name will be used instead.
        :param process_before_response: True if this app runs on Function as a Service. (Default: False)
        :param signing_secret: The Signing Secret value used for verifying requests from Slack.
        :param token: The bot access token required only for single-workspace app.
        :param client: The singleton slack_sdk.web.async_client.AsyncWebClient instance for this app.
        :param installation_store: The module offering save/find operations of installation data
        :param authorize: The function to authorize an incoming request from Slack
            by checking if there is a team/user in the installation data.
        :param oauth_settings: The settings related to Slack app installation flow (OAuth flow)
        :param oauth_flow: Manually instantiated slack_bolt.oauth.async_oauth_flow.AsyncOAuthFlow.
            This is always prioritized over oauth_settings.
        :param verification_token: Deprecated verification mechanism.
            This can used only for ssl_check requests.
        """
        signing_secret = signing_secret or os.environ.get(
            "SLACK_SIGNING_SECRET")
        token = token or os.environ.get("SLACK_BOT_TOKEN")

        self._name: str = name or inspect.stack()[1].filename.split(
            os.path.sep)[-1]
        self._signing_secret: str = signing_secret
        self._verification_token: Optional[
            str] = verification_token or os.environ.get(
                "SLACK_VERIFICATION_TOKEN", None)
        self._framework_logger = logger or get_bolt_logger(AsyncApp)

        self._token: Optional[str] = token

        if client is not None:
            if not isinstance(client, AsyncWebClient):
                raise BoltError(error_client_invalid_type_async())
            self._async_client = client
            self._token = client.token
            if token is not None:
                self._framework_logger.warning(
                    warning_client_prioritized_and_token_skipped())
        else:
            # NOTE: the token here can be None
            self._async_client = create_async_web_client(token)

        self._async_authorize: Optional[AsyncAuthorize] = None
        if authorize is not None:
            if oauth_settings is not None or oauth_flow is not None:
                raise BoltError(error_authorize_conflicts())

            self._async_authorize = AsyncCallableAuthorize(
                logger=self._framework_logger, func=authorize)

        self._async_installation_store: Optional[
            AsyncInstallationStore] = installation_store
        if self._async_installation_store is not None and self._async_authorize is None:
            self._async_authorize = AsyncInstallationStoreAuthorize(
                installation_store=self._async_installation_store,
                logger=self._framework_logger,
            )

        self._async_oauth_flow: Optional[AsyncOAuthFlow] = None

        if (oauth_settings is None
                and os.environ.get("SLACK_CLIENT_ID") is not None
                and os.environ.get("SLACK_CLIENT_SECRET") is not None):
            # initialize with the default settings
            oauth_settings = AsyncOAuthSettings()

        if oauth_flow:
            if not isinstance(oauth_flow, AsyncOAuthFlow):
                raise BoltError(error_oauth_flow_invalid_type_async())

            self._async_oauth_flow = oauth_flow
            installation_store = select_consistent_installation_store(
                client_id=self._async_oauth_flow.client_id,
                app_store=self._async_installation_store,
                oauth_flow_store=self._async_oauth_flow.settings.
                installation_store,
                logger=self._framework_logger,
            )
            self._async_installation_store = installation_store
            self._async_oauth_flow.settings.installation_store = installation_store

            if self._async_oauth_flow._async_client is None:
                self._async_oauth_flow._async_client = self._async_client
            if self._async_authorize is None:
                self._async_authorize = self._async_oauth_flow.settings.authorize
        elif oauth_settings is not None:
            if not isinstance(oauth_settings, AsyncOAuthSettings):
                raise BoltError(error_oauth_settings_invalid_type_async())

            installation_store = select_consistent_installation_store(
                client_id=oauth_settings.client_id,
                app_store=self._async_installation_store,
                oauth_flow_store=oauth_settings.installation_store,
                logger=self._framework_logger,
            )
            self._async_installation_store = installation_store
            oauth_settings.installation_store = installation_store

            self._async_oauth_flow = AsyncOAuthFlow(client=self._async_client,
                                                    logger=self.logger,
                                                    settings=oauth_settings)
            if self._async_authorize is None:
                self._async_authorize = self._async_oauth_flow.settings.authorize

        if (self._async_installation_store is not None or self._async_authorize
                is not None) and self._token is not None:
            self._token = None
            self._framework_logger.warning(warning_token_skipped())

        self._async_middleware_list: List[Union[Callable,
                                                AsyncMiddleware]] = []
        self._async_listeners: List[AsyncListener] = []

        self._async_listener_runner = AsyncioListenerRunner(
            logger=self._framework_logger,
            process_before_response=process_before_response,
            listener_error_handler=AsyncDefaultListenerErrorHandler(
                logger=self._framework_logger),
            lazy_listener_runner=AsyncioLazyListenerRunner(
                logger=self._framework_logger, ),
        )

        self._init_middleware_list_done = False
        self._init_async_middleware_list()

        self._server: Optional[AsyncSlackAppServer] = None
Ejemplo n.º 22
0
    database_url=database_url,
    logger=logger,
)
oauth_state_store = AsyncSQLAlchemyOAuthStateStore(
    expiration_seconds=120,
    database_url=database_url,
    logger=logger,
)

app = AsyncApp(
    logger=logger,
    signing_secret=signing_secret,
    installation_store=installation_store,
    oauth_settings=AsyncOAuthSettings(
        client_id=client_id,
        client_secret=client_secret,
        state_store=oauth_state_store,
    ),
)
app_handler = AsyncSlackRequestHandler(app)


@app.event("app_mention")
async def handle_command(say: AsyncSay):
    await say("Hi!")


from sanic import Sanic
from sanic.request import Request

api = Sanic(name="awesome-slack-app")