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
4 changes: 4 additions & 0 deletions examples/google_cloud_functions/.env.yaml.oauth-sample
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'
3 changes: 2 additions & 1 deletion examples/google_cloud_functions/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env.yaml
.env.yaml
main.py
252 changes: 252 additions & 0 deletions examples/google_cloud_functions/datastore.py
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
66 changes: 66 additions & 0 deletions examples/google_cloud_functions/handler.py
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)
Loading