Skip to content

Commit

Permalink
Fix #254 Add built-in tokens_revoked/app_uninstalled event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed Apr 16, 2021
1 parent 8babac6 commit 8786783
Show file tree
Hide file tree
Showing 8 changed files with 496 additions and 2 deletions.
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

0 comments on commit 8786783

Please sign in to comment.