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
73 changes: 73 additions & 0 deletions slack_sdk/oauth/installation_store/amazon_s3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,76 @@ def find_installation(
message = f"Failed to find an installation data for enterprise: {e_id}, team: {t_id}: {e}"
self.logger.warning(message)
return None

async def async_delete_bot(
self, *, enterprise_id: Optional[str], team_id: Optional[str]
) -> None:
return self.delete_bot(
enterprise_id=enterprise_id,
team_id=team_id,
)

def delete_bot(
self, *, enterprise_id: Optional[str], team_id: Optional[str]
) -> None:
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
workspace_path = f"{self.client_id}/{e_id}-{t_id}"
objects = self.s3_client.list_objects(
Bucket=self.bucket_name,
Prefix=f"{workspace_path}/bot-",
)
for content in objects.get("Contents", []):
key = content.get("Key")
if key is not None:
self.logger.info(f"Going to delete bot installation ({key})")
try:
self.s3_client.delete_object(
Bucket=self.bucket_name,
Key=content.get("Key"),
)
except Exception as e: # skipcq: PYL-W0703
message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
self.logger.warning(message)

async def async_delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
return self.delete_installation(
enterprise_id=enterprise_id,
team_id=team_id,
user_id=user_id,
)

def delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
workspace_path = f"{self.client_id}/{e_id}-{t_id}"
objects = self.s3_client.list_objects(
Bucket=self.bucket_name,
Prefix=f"{workspace_path}/installer-{user_id or ''}",
)
for content in objects.get("Contents", []):
key = content.get("Key")
if key is not None:
self.logger.info(f"Going to delete installation ({key})")
try:
self.s3_client.delete_object(
Bucket=self.bucket_name,
Key=content.get("Key"),
)
except Exception as e: # skipcq: PYL-W0703
message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
self.logger.warning(message)
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def async_find_bot(
) -> Optional[Bot]:
if is_enterprise_install or team_id is None:
team_id = ""
key = f"{enterprise_id}-{team_id}"
key = f"{enterprise_id or ''}-{team_id or ''}"
if key in self.cached_bots:
return self.cached_bots[key]
bot = await self.underlying.async_find_bot(
Expand All @@ -60,7 +60,7 @@ async def async_find_installation(
) -> Optional[Installation]:
if is_enterprise_install or team_id is None:
team_id = ""
key = f"{enterprise_id}-{team_id}-{user_id}"
key = f"{enterprise_id or ''}-{team_id or ''}-{user_id or ''}"
if key in self.cached_installations:
return self.cached_installations[key]
installation = await self.underlying.async_find_installation(
Expand All @@ -72,3 +72,49 @@ async def async_find_installation(
if installation:
self.cached_installations[key] = installation
return installation

async def async_delete_bot(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
) -> None:
await self.underlying.async_delete_bot(
enterprise_id=enterprise_id,
team_id=team_id,
)
key = f"{enterprise_id or ''}-{team_id or ''}"
self.cached_bots.pop(key)

async def async_delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
await self.underlying.async_delete_installation(
enterprise_id=enterprise_id,
team_id=team_id,
user_id=user_id,
)
key = f"{enterprise_id or ''}-{team_id or ''}={user_id or ''}"
self.cached_installations.pop(key)

async def async_delete_all(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
):
await self.underlying.async_delete_all(
enterprise_id=enterprise_id,
team_id=team_id,
)
key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
for key in self.cached_bots.keys():
if key.startswith(key_prefix):
self.cached_bots.pop(key)
for key in self.cached_installations.keys():
if key.startswith(key_prefix):
self.cached_installations.pop(key)
57 changes: 57 additions & 0 deletions slack_sdk/oauth/installation_store/async_installation_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,33 @@


class AsyncInstallationStore:
"""The installation store interface for asyncio-based apps.

The minimum required methods are:

* async_save(installation)
* async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install)

If you would like to properly handle app uninstallations and token revocations,
the following methods should be implemented.

* async_delete_installation(enterprise_id, team_id, user_id)
* async_delete_all(enterprise_id, team_id)

If your app needs only bot scope installations, the simpler way to implement would be:

* async_save(installation)
* async_find_bot(enterprise_id, team_id, is_enterprise_install)
* async_delete_bot(enterprise_id, team_id)
* async_delete_all(enterprise_id, team_id)
"""

@property
def logger(self) -> Logger:
raise NotImplementedError()

async def async_save(self, installation: Installation):
"""Saves a new installation data"""
raise NotImplementedError()

async def async_find_bot(
Expand All @@ -20,6 +42,7 @@ async def async_find_bot(
team_id: Optional[str],
is_enterprise_install: Optional[bool] = False,
) -> Optional[Bot]:
"""Finds a bot scope installation per workspace / org"""
raise NotImplementedError()

async def async_find_installation(
Expand All @@ -30,4 +53,38 @@ async def async_find_installation(
user_id: Optional[str] = None,
is_enterprise_install: Optional[bool] = False,
) -> Optional[Installation]:
"""Finds a relevant installation for the given IDs.
If the user_id is absent, this method may return the latest installation in the workspace / org.
"""
raise NotImplementedError()

async def async_delete_bot(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
) -> None:
"""Deletes a bot scope installation per workspace / org"""
raise NotImplementedError()

async def async_delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
"""Deletes an installation that matches the given IDs"""
raise NotImplementedError()

async def async_delete_all(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
):
"""Deletes all installation data for the given workspace / org"""
await self.async_delete_bot(enterprise_id=enterprise_id, team_id=team_id)
await self.async_delete_installation(
enterprise_id=enterprise_id, team_id=team_id
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def find_bot(
) -> Optional[Bot]:
if is_enterprise_install or team_id is None:
team_id = ""
key = f"{enterprise_id}-{team_id}"
key = f"{enterprise_id or ''}-{team_id or ''}"
if key in self.cached_bots:
return self.cached_bots[key]
bot = self.underlying.find_bot(
Expand All @@ -58,7 +58,7 @@ def find_installation(
) -> Optional[Installation]:
if is_enterprise_install or team_id is None:
team_id = ""
key = f"{enterprise_id}-{team_id}={user_id}"
key = f"{enterprise_id or ''}-{team_id or ''}={user_id or ''}"
if key in self.cached_installations:
return self.cached_installations[key]
installation = self.underlying.find_installation(
Expand All @@ -70,3 +70,49 @@ def find_installation(
if installation:
self.cached_installations[key] = installation
return installation

def delete_bot(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
) -> None:
self.underlying.delete_bot(
enterprise_id=enterprise_id,
team_id=team_id,
)
key = f"{enterprise_id or ''}-{team_id or ''}"
self.cached_bots.pop(key)

def delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
self.underlying.delete_installation(
enterprise_id=enterprise_id,
team_id=team_id,
user_id=user_id,
)
key = f"{enterprise_id or ''}-{team_id or ''}={user_id or ''}"
self.cached_installations.pop(key)

def delete_all(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
):
self.underlying.delete_all(
enterprise_id=enterprise_id,
team_id=team_id,
)
key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
for key in self.cached_bots.keys():
if key.startswith(key_prefix):
self.cached_bots.pop(key)
for key in self.cached_installations.keys():
if key.startswith(key_prefix):
self.cached_installations.pop(key)
51 changes: 51 additions & 0 deletions slack_sdk/oauth/installation_store/file/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import glob
import json
import logging
import os
from logging import Logger
from pathlib import Path
from typing import Optional, Union
Expand Down Expand Up @@ -159,6 +161,55 @@ def find_installation(
self.logger.warning(message)
return None

async def async_delete_bot(
self, *, enterprise_id: Optional[str], team_id: Optional[str]
) -> None:
return self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)

def delete_bot(
self, *, enterprise_id: Optional[str], team_id: Optional[str]
) -> None:
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/bot-*"
self._delete_by_glob(e_id, t_id, filepath_glob)

async def async_delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
return self.delete_installation(
enterprise_id=enterprise_id, team_id=team_id, user_id=user_id
)

def delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
) -> None:
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
if user_id is not None:
filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-*"
else:
filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-*"
self._delete_by_glob(e_id, t_id, filepath_glob)

def _delete_by_glob(self, e_id: str, t_id: str, filepath_glob: str):
for filepath in glob.glob(filepath_glob):
try:
os.remove(filepath)
except FileNotFoundError as e:
message = f"Failed to delete installation data for enterprise: {e_id}, team: {t_id}: {e}"
self.logger.warning(message)

@staticmethod
def _mkdir(path: Union[str, Path]):
if isinstance(path, str):
Expand Down
Loading