Skip to content

Commit 8786783

Browse files
committed
Fix #254 Add built-in tokens_revoked/app_uninstalled event handlers
1 parent 8babac6 commit 8786783

File tree

8 files changed

+496
-2
lines changed

8 files changed

+496
-2
lines changed

slack_bolt/app/app.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from slack_bolt.error import BoltError
2121
from slack_bolt.lazy_listener.thread_runner import ThreadLazyListenerRunner
22+
from slack_bolt.listener.builtins import TokenRevocationListeners
2223
from slack_bolt.listener.custom_listener import CustomListener
2324
from slack_bolt.listener.listener import Listener
2425
from slack_bolt.listener.listener_completion_handler import (
@@ -48,6 +49,7 @@
4849
warning_bot_only_conflicts,
4950
debug_return_listener_middleware_response,
5051
info_default_oauth_settings_loaded,
52+
error_installation_store_required_for_builtin_listeners,
5153
)
5254
from slack_bolt.middleware import (
5355
Middleware,
@@ -250,6 +252,12 @@ def message_hello(message, say):
250252
self._oauth_flow.settings.installation_store_bot_only = app_bot_only
251253
self._authorize.bot_only = app_bot_only
252254

255+
self._tokens_revocation_listeners: Optional[TokenRevocationListeners] = None
256+
if self._installation_store is not None:
257+
self._tokens_revocation_listeners = TokenRevocationListeners(
258+
self._installation_store
259+
)
260+
253261
# --------------------------------------
254262
# Middleware Initialization
255263
# --------------------------------------
@@ -1089,6 +1097,27 @@ def __call__(*args, **kwargs):
10891097

10901098
return __call__
10911099

1100+
# -------------------------
1101+
# built-in listener functions
1102+
1103+
def default_tokens_revoked_event_listener(
1104+
self,
1105+
) -> Callable[..., Optional[BoltResponse]]:
1106+
if self._tokens_revocation_listeners is None:
1107+
raise BoltError(error_installation_store_required_for_builtin_listeners())
1108+
return self._tokens_revocation_listeners.handle_tokens_revoked_events
1109+
1110+
def default_app_uninstalled_event_listener(
1111+
self,
1112+
) -> Callable[..., Optional[BoltResponse]]:
1113+
if self._tokens_revocation_listeners is None:
1114+
raise BoltError(error_installation_store_required_for_builtin_listeners())
1115+
return self._tokens_revocation_listeners.handle_app_uninstalled_events
1116+
1117+
def enable_token_revocation_listeners(self) -> None:
1118+
self.event("tokens_revoked")(self.default_tokens_revoked_event_listener())
1119+
self.event("app_uninstalled")(self.default_app_uninstalled_event_listener())
1120+
10921121
# -------------------------
10931122

10941123
def _init_context(self, req: BoltRequest):

slack_bolt/app/async_app.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aiohttp import web
88

99
from slack_bolt.app.async_server import AsyncSlackAppServer
10+
from slack_bolt.listener.async_builtins import AsyncTokenRevocationListeners
1011
from slack_bolt.listener.async_listener_completion_handler import (
1112
AsyncDefaultListenerCompletionHandler,
1213
)
@@ -49,6 +50,7 @@
4950
warning_bot_only_conflicts,
5051
debug_return_listener_middleware_response,
5152
info_default_oauth_settings_loaded,
53+
error_installation_store_required_for_builtin_listeners,
5254
)
5355
from slack_bolt.lazy_listener.asyncio_runner import AsyncioLazyListenerRunner
5456
from slack_bolt.listener.async_listener import AsyncListener, AsyncCustomListener
@@ -275,6 +277,14 @@ async def message_hello(message, say): # async function
275277
)
276278
self._async_authorize.bot_only = app_bot_only
277279

280+
self._async_tokens_revocation_listeners: Optional[
281+
AsyncTokenRevocationListeners
282+
] = None
283+
if self._async_installation_store is not None:
284+
self._async_tokens_revocation_listeners = AsyncTokenRevocationListeners(
285+
self._async_installation_store
286+
)
287+
278288
# --------------------------------------
279289
# Middleware Initialization
280290
# --------------------------------------
@@ -1151,6 +1161,27 @@ def __call__(*args, **kwargs):
11511161

11521162
return __call__
11531163

1164+
# -------------------------
1165+
# built-in listener functions
1166+
1167+
def default_tokens_revoked_event_listener(
1168+
self,
1169+
) -> Callable[..., Awaitable[Optional[BoltResponse]]]:
1170+
if self._async_tokens_revocation_listeners is None:
1171+
raise BoltError(error_installation_store_required_for_builtin_listeners())
1172+
return self._async_tokens_revocation_listeners.handle_tokens_revoked_events
1173+
1174+
def default_app_uninstalled_event_listener(
1175+
self,
1176+
) -> Callable[..., Awaitable[Optional[BoltResponse]]]:
1177+
if self._async_tokens_revocation_listeners is None:
1178+
raise BoltError(error_installation_store_required_for_builtin_listeners())
1179+
return self._async_tokens_revocation_listeners.handle_app_uninstalled_events
1180+
1181+
def enable_token_revocation_listeners(self) -> None:
1182+
self.event("tokens_revoked")(self.default_tokens_revoked_event_listener())
1183+
self.event("app_uninstalled")(self.default_app_uninstalled_event_listener())
1184+
11541185
# -------------------------
11551186

11561187
def _init_context(self, req: AsyncBoltRequest):
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from slack_bolt.context.async_context import AsyncBoltContext
2+
from slack_sdk.oauth.installation_store.async_installation_store import (
3+
AsyncInstallationStore,
4+
)
5+
6+
7+
class AsyncTokenRevocationListeners:
8+
"""Listener functions to handle token revocation / uninstallation events"""
9+
10+
installation_store: AsyncInstallationStore
11+
12+
def __init__(self, installation_store: AsyncInstallationStore):
13+
self.installation_store = installation_store
14+
15+
async def handle_tokens_revoked_events(
16+
self, event: dict, context: AsyncBoltContext
17+
) -> None:
18+
user_ids = event.get("tokens", {}).get("oauth", [])
19+
if len(user_ids) > 0:
20+
for user_id in user_ids:
21+
await self.installation_store.async_delete_installation(
22+
enterprise_id=context.enterprise_id,
23+
team_id=context.team_id,
24+
user_id=user_id,
25+
)
26+
bots = event.get("tokens", {}).get("bot", [])
27+
if len(bots) > 0:
28+
await self.installation_store.async_delete_bot(
29+
enterprise_id=context.enterprise_id,
30+
team_id=context.team_id,
31+
)
32+
33+
async def handle_app_uninstalled_events(self, context: AsyncBoltContext) -> None:
34+
await self.installation_store.async_delete_all(
35+
enterprise_id=context.enterprise_id,
36+
team_id=context.team_id,
37+
)

slack_bolt/listener/builtins.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from slack_sdk.oauth import InstallationStore
2+
3+
from slack_bolt.context.context import BoltContext
4+
from slack_sdk.oauth.installation_store.installation_store import InstallationStore
5+
6+
7+
class TokenRevocationListeners:
8+
"""Listener functions to handle token revocation / uninstallation events"""
9+
10+
installation_store: InstallationStore
11+
12+
def __init__(self, installation_store: InstallationStore):
13+
self.installation_store = installation_store
14+
15+
def handle_tokens_revoked_events(self, event: dict, context: BoltContext) -> None:
16+
user_ids = event.get("tokens", {}).get("oauth", [])
17+
if len(user_ids) > 0:
18+
for user_id in user_ids:
19+
self.installation_store.delete_installation(
20+
enterprise_id=context.enterprise_id,
21+
team_id=context.team_id,
22+
user_id=user_id,
23+
)
24+
bots = event.get("tokens", {}).get("bot", [])
25+
if len(bots) > 0:
26+
self.installation_store.delete_bot(
27+
enterprise_id=context.enterprise_id,
28+
team_id=context.team_id,
29+
)
30+
31+
def handle_app_uninstalled_events(self, context: BoltContext) -> None:
32+
self.installation_store.delete_all(
33+
enterprise_id=context.enterprise_id,
34+
team_id=context.team_id,
35+
)

slack_bolt/logger/messages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def error_message_event_type(event_type: str) -> str:
5858
)
5959

6060

61+
def error_installation_store_required_for_builtin_listeners() -> str:
62+
return (
63+
"To use the event listeners for token revocation handling, "
64+
"setting a valid `installation_store` to `App`/`AsyncApp` is required."
65+
)
66+
67+
6168
# -------------------------------
6269
# Warning
6370
# -------------------------------

tests/mock_web_api_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def assert_auth_test_count(test: TestCase, expected_count: int):
310310
error = None
311311
while retry_count < 3:
312312
try:
313-
test.mock_received_requests["/auth.test"] == expected_count
313+
test.mock_received_requests.get("/auth.test", 0) == expected_count
314314
break
315315
except Exception as e:
316316
error = e
@@ -328,7 +328,7 @@ async def assert_auth_test_count_async(test: TestCase, expected_count: int):
328328
error = None
329329
while retry_count < 3:
330330
try:
331-
test.mock_received_requests["/auth.test"] == expected_count
331+
test.mock_received_requests.get("/auth.test", 0) == expected_count
332332
break
333333
except Exception as e:
334334
error = e

0 commit comments

Comments
 (0)