Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from slack_bolt.error import BoltError
from slack_bolt.lazy_listener.thread_runner import ThreadLazyListenerRunner
from slack_bolt.listener.builtins import TokenRevocationListeners
from slack_bolt.listener.custom_listener import CustomListener
from slack_bolt.listener.listener import Listener
from slack_bolt.listener.listener_completion_handler import (
Expand Down Expand Up @@ -48,6 +49,7 @@
warning_bot_only_conflicts,
debug_return_listener_middleware_response,
info_default_oauth_settings_loaded,
error_installation_store_required_for_builtin_listeners,
)
from slack_bolt.middleware import (
Middleware,
Expand Down Expand Up @@ -250,6 +252,12 @@ def message_hello(message, say):
self._oauth_flow.settings.installation_store_bot_only = app_bot_only
self._authorize.bot_only = app_bot_only

self._tokens_revocation_listeners: Optional[TokenRevocationListeners] = None
if self._installation_store is not None:
self._tokens_revocation_listeners = TokenRevocationListeners(
self._installation_store
)

# --------------------------------------
# Middleware Initialization
# --------------------------------------
Expand Down Expand Up @@ -1089,6 +1097,27 @@ def __call__(*args, **kwargs):

return __call__

# -------------------------
# built-in listener functions

def default_tokens_revoked_event_listener(
self,
) -> Callable[..., Optional[BoltResponse]]:
if self._tokens_revocation_listeners is None:
raise BoltError(error_installation_store_required_for_builtin_listeners())
return self._tokens_revocation_listeners.handle_tokens_revoked_events

def default_app_uninstalled_event_listener(
self,
) -> Callable[..., Optional[BoltResponse]]:
if self._tokens_revocation_listeners is None:
raise BoltError(error_installation_store_required_for_builtin_listeners())
return self._tokens_revocation_listeners.handle_app_uninstalled_events

def enable_token_revocation_listeners(self) -> None:
self.event("tokens_revoked")(self.default_tokens_revoked_event_listener())
self.event("app_uninstalled")(self.default_app_uninstalled_event_listener())

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

def _init_context(self, req: BoltRequest):
Expand Down
31 changes: 31 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from aiohttp import web

from slack_bolt.app.async_server import AsyncSlackAppServer
from slack_bolt.listener.async_builtins import AsyncTokenRevocationListeners
from slack_bolt.listener.async_listener_completion_handler import (
AsyncDefaultListenerCompletionHandler,
)
Expand Down Expand Up @@ -49,6 +50,7 @@
warning_bot_only_conflicts,
debug_return_listener_middleware_response,
info_default_oauth_settings_loaded,
error_installation_store_required_for_builtin_listeners,
)
from slack_bolt.lazy_listener.asyncio_runner import AsyncioLazyListenerRunner
from slack_bolt.listener.async_listener import AsyncListener, AsyncCustomListener
Expand Down Expand Up @@ -275,6 +277,14 @@ async def message_hello(message, say): # async function
)
self._async_authorize.bot_only = app_bot_only

self._async_tokens_revocation_listeners: Optional[
AsyncTokenRevocationListeners
] = None
if self._async_installation_store is not None:
self._async_tokens_revocation_listeners = AsyncTokenRevocationListeners(
self._async_installation_store
)

# --------------------------------------
# Middleware Initialization
# --------------------------------------
Expand Down Expand Up @@ -1151,6 +1161,27 @@ def __call__(*args, **kwargs):

return __call__

# -------------------------
# built-in listener functions

def default_tokens_revoked_event_listener(
self,
) -> Callable[..., Awaitable[Optional[BoltResponse]]]:
if self._async_tokens_revocation_listeners is None:
raise BoltError(error_installation_store_required_for_builtin_listeners())
return self._async_tokens_revocation_listeners.handle_tokens_revoked_events

def default_app_uninstalled_event_listener(
self,
) -> Callable[..., Awaitable[Optional[BoltResponse]]]:
if self._async_tokens_revocation_listeners is None:
raise BoltError(error_installation_store_required_for_builtin_listeners())
return self._async_tokens_revocation_listeners.handle_app_uninstalled_events

def enable_token_revocation_listeners(self) -> None:
self.event("tokens_revoked")(self.default_tokens_revoked_event_listener())
self.event("app_uninstalled")(self.default_app_uninstalled_event_listener())

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

def _init_context(self, req: AsyncBoltRequest):
Expand Down
37 changes: 37 additions & 0 deletions slack_bolt/listener/async_builtins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from slack_bolt.context.async_context import AsyncBoltContext
from slack_sdk.oauth.installation_store.async_installation_store import (
AsyncInstallationStore,
)


class AsyncTokenRevocationListeners:
"""Listener functions to handle token revocation / uninstallation events"""

installation_store: AsyncInstallationStore

def __init__(self, installation_store: AsyncInstallationStore):
self.installation_store = installation_store

async def handle_tokens_revoked_events(
self, event: dict, context: AsyncBoltContext
) -> None:
user_ids = event.get("tokens", {}).get("oauth", [])
if len(user_ids) > 0:
for user_id in user_ids:
await self.installation_store.async_delete_installation(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
user_id=user_id,
)
bots = event.get("tokens", {}).get("bot", [])
if len(bots) > 0:
await self.installation_store.async_delete_bot(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)

async def handle_app_uninstalled_events(self, context: AsyncBoltContext) -> None:
await self.installation_store.async_delete_all(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
35 changes: 35 additions & 0 deletions slack_bolt/listener/builtins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from slack_sdk.oauth import InstallationStore

from slack_bolt.context.context import BoltContext
from slack_sdk.oauth.installation_store.installation_store import InstallationStore


class TokenRevocationListeners:
"""Listener functions to handle token revocation / uninstallation events"""

installation_store: InstallationStore

def __init__(self, installation_store: InstallationStore):
self.installation_store = installation_store

def handle_tokens_revoked_events(self, event: dict, context: BoltContext) -> None:
user_ids = event.get("tokens", {}).get("oauth", [])
if len(user_ids) > 0:
for user_id in user_ids:
self.installation_store.delete_installation(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
user_id=user_id,
)
bots = event.get("tokens", {}).get("bot", [])
if len(bots) > 0:
self.installation_store.delete_bot(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)

def handle_app_uninstalled_events(self, context: BoltContext) -> None:
self.installation_store.delete_all(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
7 changes: 7 additions & 0 deletions slack_bolt/logger/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def error_message_event_type(event_type: str) -> str:
)


def error_installation_store_required_for_builtin_listeners() -> str:
return (
"To use the event listeners for token revocation handling, "
"setting a valid `installation_store` to `App`/`AsyncApp` is required."
)


# -------------------------------
# Warning
# -------------------------------
Expand Down
4 changes: 2 additions & 2 deletions tests/mock_web_api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def assert_auth_test_count(test: TestCase, expected_count: int):
error = None
while retry_count < 3:
try:
test.mock_received_requests["/auth.test"] == expected_count
test.mock_received_requests.get("/auth.test", 0) == expected_count
break
except Exception as e:
error = e
Expand All @@ -328,7 +328,7 @@ async def assert_auth_test_count_async(test: TestCase, expected_count: int):
error = None
while retry_count < 3:
try:
test.mock_received_requests["/auth.test"] == expected_count
test.mock_received_requests.get("/auth.test", 0) == expected_count
break
except Exception as e:
error = e
Expand Down
Loading