-
Notifications
You must be signed in to change notification settings - Fork 248
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
11 changed files
with
742 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
SLACK_CLIENT_ID: '1111.222' | ||
SLACK_CLIENT_SECRET: 'xxx' | ||
SLACK_SIGNING_SECRET: 'yyy' | ||
SLACK_SCOPES: 'app_mentions:read,chat:write,commands' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
.env.yaml | ||
.env.yaml | ||
main.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
# | ||
# Please note that this is an example implementation. | ||
# You can reuse this implementation for your app, | ||
# but we don't have short-term plans to add this code to slack-sdk package. | ||
# Please maintain the code on your own if you copy this file. | ||
# | ||
# Also, please refer to the following gist for more discussion and better implementation: | ||
# https://gist.github.com/seratch/d81a445ef4467b16f047156bf859cda8 | ||
# | ||
|
||
import logging | ||
from logging import Logger | ||
from typing import Optional | ||
from uuid import uuid4 | ||
|
||
from google.cloud import datastore | ||
from google.cloud.datastore import Client, Entity, Query | ||
from slack_sdk.oauth import OAuthStateStore, InstallationStore | ||
from slack_sdk.oauth.installation_store import Installation, Bot | ||
|
||
|
||
class GoogleDatastoreInstallationStore(InstallationStore): | ||
datastore_client: Client | ||
|
||
def __init__( | ||
self, | ||
*, | ||
datastore_client: Client, | ||
logger: Logger, | ||
): | ||
self.datastore_client = datastore_client | ||
self._logger = logger | ||
|
||
@property | ||
def logger(self) -> Logger: | ||
if self._logger is None: | ||
self._logger = logging.getLogger(__name__) | ||
return self._logger | ||
|
||
def installation_key( | ||
self, | ||
*, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
user_id: Optional[str], | ||
suffix: Optional[str] = None, | ||
is_enterprise_install: Optional[bool] = None, | ||
): | ||
enterprise_id = enterprise_id or "none" | ||
team_id = "none" if is_enterprise_install else team_id or "none" | ||
name = ( | ||
f"{enterprise_id}-{team_id}-{user_id}" | ||
if user_id | ||
else f"{enterprise_id}-{team_id}" | ||
) | ||
if suffix is not None: | ||
name += "-" + suffix | ||
return self.datastore_client.key("installations", name) | ||
|
||
def bot_key( | ||
self, | ||
*, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
suffix: Optional[str] = None, | ||
is_enterprise_install: Optional[bool] = None, | ||
): | ||
enterprise_id = enterprise_id or "none" | ||
team_id = "none" if is_enterprise_install else team_id or "none" | ||
name = f"{enterprise_id}-{team_id}" | ||
if suffix is not None: | ||
name += "-" + suffix | ||
return self.datastore_client.key("bots", name) | ||
|
||
def save(self, i: Installation): | ||
# the latest installation in the workspace | ||
installation_entity: Entity = datastore.Entity( | ||
key=self.installation_key( | ||
enterprise_id=i.enterprise_id, | ||
team_id=i.team_id, | ||
user_id=None, # user_id is removed | ||
is_enterprise_install=i.is_enterprise_install, | ||
) | ||
) | ||
installation_entity.update(**i.to_dict()) | ||
self.datastore_client.put(installation_entity) | ||
|
||
# the latest installation associated with a user | ||
user_entity: Entity = datastore.Entity( | ||
key=self.installation_key( | ||
enterprise_id=i.enterprise_id, | ||
team_id=i.team_id, | ||
user_id=i.user_id, | ||
is_enterprise_install=i.is_enterprise_install, | ||
) | ||
) | ||
user_entity.update(**i.to_dict()) | ||
self.datastore_client.put(user_entity) | ||
# history data | ||
user_entity.key = self.installation_key( | ||
enterprise_id=i.enterprise_id, | ||
team_id=i.team_id, | ||
user_id=i.user_id, | ||
is_enterprise_install=i.is_enterprise_install, | ||
suffix=str(i.installed_at), | ||
) | ||
self.datastore_client.put(user_entity) | ||
|
||
# the latest bot authorization in the workspace | ||
bot = i.to_bot() | ||
bot_entity: Entity = datastore.Entity( | ||
key=self.bot_key( | ||
enterprise_id=i.enterprise_id, | ||
team_id=i.team_id, | ||
is_enterprise_install=i.is_enterprise_install, | ||
) | ||
) | ||
bot_entity.update(**bot.to_dict()) | ||
self.datastore_client.put(bot_entity) | ||
# history data | ||
bot_entity.key = self.bot_key( | ||
enterprise_id=i.enterprise_id, | ||
team_id=i.team_id, | ||
is_enterprise_install=i.is_enterprise_install, | ||
suffix=str(i.installed_at), | ||
) | ||
self.datastore_client.put(bot_entity) | ||
|
||
def find_bot( | ||
self, | ||
*, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
is_enterprise_install: Optional[bool] = False, | ||
) -> Optional[Bot]: | ||
entity: Entity = self.datastore_client.get( | ||
self.bot_key( | ||
enterprise_id=enterprise_id, | ||
team_id=team_id, | ||
is_enterprise_install=is_enterprise_install, | ||
) | ||
) | ||
if entity is not None: | ||
entity["installed_at"] = entity["installed_at"].timestamp() | ||
return Bot(**entity) | ||
return None | ||
|
||
def find_installation( | ||
self, | ||
*, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
user_id: Optional[str] = None, | ||
is_enterprise_install: Optional[bool] = False, | ||
) -> Optional[Installation]: | ||
entity: Entity = self.datastore_client.get( | ||
self.installation_key( | ||
enterprise_id=enterprise_id, | ||
team_id=team_id, | ||
user_id=user_id, | ||
is_enterprise_install=is_enterprise_install, | ||
) | ||
) | ||
if entity is not None: | ||
entity["installed_at"] = entity["installed_at"].timestamp() | ||
return Installation(**entity) | ||
return None | ||
|
||
def delete_installation( | ||
self, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
user_id: Optional[str], | ||
) -> None: | ||
installation_key = self.installation_key( | ||
enterprise_id=enterprise_id, | ||
team_id=team_id, | ||
user_id=user_id, | ||
) | ||
q: Query = self.datastore_client.query() | ||
q.key_filter(installation_key, ">=") | ||
for entity in q.fetch(): | ||
if entity.key.name.startswith(installation_key.name): | ||
self.datastore_client.delete(entity.key) | ||
else: | ||
break | ||
|
||
def delete_bot( | ||
self, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
) -> None: | ||
bot_key = self.bot_key( | ||
enterprise_id=enterprise_id, | ||
team_id=team_id, | ||
) | ||
q: Query = self.datastore_client.query() | ||
q.key_filter(bot_key, ">=") | ||
for entity in q.fetch(): | ||
if entity.key.name.startswith(bot_key.name): | ||
self.datastore_client.delete(entity.key) | ||
else: | ||
break | ||
|
||
def delete_all( | ||
self, | ||
enterprise_id: Optional[str], | ||
team_id: Optional[str], | ||
): | ||
self.delete_bot(enterprise_id=enterprise_id, team_id=team_id) | ||
self.delete_installation( | ||
enterprise_id=enterprise_id, team_id=team_id, user_id=None | ||
) | ||
|
||
|
||
class GoogleDatastoreOAuthStateStore(OAuthStateStore): | ||
logger: Logger | ||
datastore_client: Client | ||
collection_id: str | ||
|
||
def __init__( | ||
self, | ||
*, | ||
datastore_client: Client, | ||
logger: Logger, | ||
): | ||
self.datastore_client = datastore_client | ||
self._logger = logger | ||
self.collection_id = "oauth_state_values" | ||
|
||
@property | ||
def logger(self) -> Logger: | ||
if self._logger is None: | ||
self._logger = logging.getLogger(__name__) | ||
return self._logger | ||
|
||
def consume(self, state: str) -> bool: | ||
key = self.datastore_client.key(self.collection_id, state) | ||
entity = self.datastore_client.get(key) | ||
if entity is not None: | ||
self.datastore_client.delete(key) | ||
return True | ||
return False | ||
|
||
def issue(self, *args, **kwargs) -> str: | ||
state_value = str(uuid4()) | ||
entity: Entity = datastore.Entity( | ||
key=self.datastore_client.key(self.collection_id, state_value) | ||
) | ||
entity.update(value=state_value) | ||
self.datastore_client.put(entity) | ||
return state_value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# TODO: Once this once a new version newer than 1.13.2, delete this file | ||
|
||
from typing import Callable | ||
|
||
from flask import Request, Response, make_response | ||
|
||
from slack_bolt.app import App | ||
from slack_bolt.error import BoltError | ||
from slack_bolt.lazy_listener import LazyListenerRunner | ||
from slack_bolt.oauth import OAuthFlow | ||
from slack_bolt.request import BoltRequest | ||
from slack_bolt.response import BoltResponse | ||
|
||
|
||
def to_bolt_request(req: Request) -> BoltRequest: | ||
return BoltRequest( # type: ignore | ||
body=req.get_data(as_text=True), | ||
query=req.query_string.decode("utf-8"), | ||
headers=req.headers, # type: ignore | ||
) # type: ignore | ||
|
||
|
||
def to_flask_response(bolt_resp: BoltResponse) -> Response: | ||
resp: Response = make_response(bolt_resp.body, bolt_resp.status) | ||
for k, values in bolt_resp.headers.items(): | ||
if k.lower() == "content-type" and resp.headers.get("content-type") is not None: | ||
# Remove the one set by Flask | ||
resp.headers.pop("content-type") | ||
for v in values: | ||
resp.headers.add_header(k, v) | ||
return resp | ||
|
||
|
||
class NoopLazyListenerRunner(LazyListenerRunner): | ||
def start(self, function: Callable[..., None], request: BoltRequest) -> None: | ||
raise BoltError( | ||
"The google_cloud_functions adapter does not support lazy listeners. " | ||
"Please consider either having a queue to pass the request to a different function or " | ||
"rewriting your code not to use lazy listeners." | ||
) | ||
|
||
|
||
class SlackRequestHandler: | ||
def __init__(self, app: App): # type: ignore | ||
self.app = app | ||
# Note that lazy listener is not supported | ||
self.app.listener_runner.lazy_listener_runner = NoopLazyListenerRunner() | ||
if self.app.oauth_flow is not None: | ||
self.app.oauth_flow.settings.redirect_uri_page_renderer.install_path = "?" | ||
|
||
def handle(self, req: Request) -> Response: | ||
if req.method == "GET": | ||
if self.app.oauth_flow is not None: | ||
oauth_flow: OAuthFlow = self.app.oauth_flow | ||
bolt_req = to_bolt_request(req) | ||
if "code" in req.args or "error" in req.args or "state" in req.args: | ||
bolt_resp = oauth_flow.handle_callback(bolt_req) | ||
return to_flask_response(bolt_resp) | ||
else: | ||
bolt_resp = oauth_flow.handle_installation(bolt_req) | ||
return to_flask_response(bolt_resp) | ||
elif req.method == "POST": | ||
bolt_resp: BoltResponse = self.app.dispatch(to_bolt_request(req)) | ||
return to_flask_response(bolt_resp) | ||
|
||
return make_response("Not Found", 404) |
Oops, something went wrong.