Skip to content

Commit 13c7a03

Browse files
committed
Add SQLAlchemy samples
1 parent 082c276 commit 13c7a03

File tree

4 files changed

+324
-0
lines changed

4 files changed

+324
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import logging
2+
import os
3+
import time
4+
from datetime import datetime
5+
from logging import Logger
6+
from typing import Optional
7+
from uuid import uuid4
8+
9+
import sqlalchemy
10+
from databases import Database
11+
from slack_sdk.oauth.installation_store import Bot, Installation
12+
from slack_sdk.oauth.installation_store.async_installation_store import (
13+
AsyncInstallationStore,
14+
)
15+
from slack_sdk.oauth.installation_store.sqlalchemy import SQLAlchemyInstallationStore
16+
from slack_sdk.oauth.state_store.async_state_store import AsyncOAuthStateStore
17+
from slack_sdk.oauth.state_store.sqlalchemy import SQLAlchemyOAuthStateStore
18+
from sqlalchemy import and_, desc, Table, MetaData
19+
20+
from slack_bolt.adapter.sanic import AsyncSlackRequestHandler
21+
from slack_bolt.async_app import AsyncApp
22+
from slack_bolt.context.say.async_say import AsyncSay
23+
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
24+
25+
26+
class AsyncSQLAlchemyInstallationStore(AsyncInstallationStore):
27+
database_url: str
28+
client_id: str
29+
metadata: MetaData
30+
installations: Table
31+
bots: Table
32+
33+
def __init__(
34+
self,
35+
client_id: str,
36+
database_url: str,
37+
logger: Logger = logging.getLogger(__name__),
38+
):
39+
self.client_id = client_id
40+
self.database_url = database_url
41+
self._logger = logger
42+
self.metadata = MetaData()
43+
self.installations = SQLAlchemyInstallationStore.build_installations_table(
44+
metadata=self.metadata,
45+
table_name=SQLAlchemyInstallationStore.default_installations_table_name,
46+
)
47+
self.bots = SQLAlchemyInstallationStore.build_bots_table(
48+
metadata=self.metadata,
49+
table_name=SQLAlchemyInstallationStore.default_bots_table_name,
50+
)
51+
52+
@property
53+
def logger(self) -> Logger:
54+
return self._logger
55+
56+
async def async_save(self, installation: Installation):
57+
async with Database(self.database_url) as database:
58+
async with database.transaction():
59+
i = installation.to_dict()
60+
i["client_id"] = self.client_id
61+
await database.execute(self.installations.insert(), i)
62+
b = installation.to_bot().to_dict()
63+
b["client_id"] = self.client_id
64+
await database.execute(self.bots.insert(), b)
65+
66+
async def async_find_bot(
67+
self, *, enterprise_id: Optional[str], team_id: Optional[str]
68+
) -> Optional[Bot]:
69+
c = self.bots.c
70+
query = (
71+
self.bots.select()
72+
.where(and_(c.enterprise_id == enterprise_id, c.team_id == team_id))
73+
.order_by(desc(c.installed_at))
74+
.limit(1)
75+
)
76+
async with Database(self.database_url) as database:
77+
result = await database.fetch_one(query)
78+
if result:
79+
return Bot(
80+
app_id=result["app_id"],
81+
enterprise_id=result["enterprise_id"],
82+
team_id=result["team_id"],
83+
bot_token=result["bot_token"],
84+
bot_id=result["bot_id"],
85+
bot_user_id=result["bot_user_id"],
86+
bot_scopes=result["bot_scopes"],
87+
installed_at=result["installed_at"],
88+
)
89+
else:
90+
return None
91+
92+
93+
class AsyncSQLAlchemyOAuthStateStore(AsyncOAuthStateStore):
94+
database_url: str
95+
expiration_seconds: int
96+
metadata: MetaData
97+
oauth_states: Table
98+
99+
def __init__(
100+
self,
101+
*,
102+
expiration_seconds: int,
103+
database_url: str,
104+
logger: Logger = logging.getLogger(__name__),
105+
):
106+
self.expiration_seconds = expiration_seconds
107+
self.database_url = database_url
108+
self._logger = logger
109+
self.metadata = MetaData()
110+
self.oauth_states = SQLAlchemyOAuthStateStore.build_oauth_states_table(
111+
metadata=self.metadata,
112+
table_name=SQLAlchemyOAuthStateStore.default_table_name,
113+
)
114+
115+
@property
116+
def logger(self) -> Logger:
117+
return self._logger
118+
119+
async def async_issue(self) -> str:
120+
state: str = str(uuid4())
121+
now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
122+
async with Database(self.database_url) as database:
123+
await database.execute(
124+
self.oauth_states.insert(), {"state": state, "expire_at": now}
125+
)
126+
return state
127+
128+
async def async_consume(self, state: str) -> bool:
129+
try:
130+
async with Database(self.database_url) as database:
131+
async with database.transaction():
132+
c = self.oauth_states.c
133+
query = self.oauth_states.select().where(
134+
and_(c.state == state, c.expire_at > datetime.utcnow())
135+
)
136+
row = await database.fetch_one(query)
137+
self.logger.debug(f"consume's query result: {row}")
138+
await database.execute(
139+
self.oauth_states.delete().where(c.id == row["id"])
140+
)
141+
return True
142+
return False
143+
except Exception as e:
144+
message = f"Failed to find any persistent data for state: {state} - {e}"
145+
self.logger.warning(message)
146+
return False
147+
148+
149+
database_url = "sqlite:///slackapp.db"
150+
# database_url = "postgresql://localhost/slackapp" # pip install psycopg2 databases[postgresql]
151+
152+
logger = logging.getLogger(__name__)
153+
client_id, client_secret, signing_secret = (
154+
os.environ["SLACK_CLIENT_ID"],
155+
os.environ["SLACK_CLIENT_SECRET"],
156+
os.environ["SLACK_SIGNING_SECRET"],
157+
)
158+
159+
installation_store = AsyncSQLAlchemyInstallationStore(
160+
client_id=client_id, database_url=database_url, logger=logger,
161+
)
162+
oauth_state_store = AsyncSQLAlchemyOAuthStateStore(
163+
expiration_seconds=120, database_url=database_url, logger=logger,
164+
)
165+
166+
app = AsyncApp(
167+
logger=logger,
168+
signing_secret=signing_secret,
169+
installation_store=installation_store,
170+
oauth_settings=AsyncOAuthSettings(
171+
client_id=client_id, client_secret=client_secret, state_store=oauth_state_store,
172+
),
173+
)
174+
app_handler = AsyncSlackRequestHandler(app)
175+
176+
177+
@app.event("app_mention")
178+
async def handle_command(say: AsyncSay):
179+
await say("Hi!")
180+
181+
182+
from sanic import Sanic
183+
from sanic.request import Request
184+
185+
api = Sanic(name="awesome-slack-app")
186+
187+
188+
@api.post("/slack/events")
189+
async def endpoint(req: Request):
190+
return await app_handler.handle(req)
191+
192+
193+
@api.get("/slack/install")
194+
async def install(req: Request):
195+
return await app_handler.handle(req)
196+
197+
198+
@api.get("/slack/oauth_redirect")
199+
async def oauth_redirect(req: Request):
200+
return await app_handler.handle(req)
201+
202+
203+
async def init():
204+
try:
205+
async with Database(database_url) as database:
206+
await database.fetch_one("select count(*) from slack_bots")
207+
except Exception:
208+
engine = sqlalchemy.create_engine(database_url)
209+
installation_store.metadata.create_all(engine)
210+
oauth_state_store.metadata.create_all(engine)
211+
212+
213+
if __name__ == "__main__":
214+
import asyncio
215+
216+
logging.basicConfig(level=logging.DEBUG)
217+
asyncio.run(init())
218+
api.run(host="0.0.0.0", port=int(os.environ.get("PORT", 3000)))
219+
220+
# pip install -r requirements_async.txt
221+
222+
# # -- OAuth flow -- #
223+
# export SLACK_SIGNING_SECRET=***
224+
# export SLACK_CLIENT_ID=111.111
225+
# export SLACK_CLIENT_SECRET=***
226+
# export SLACK_SCOPES=app_mentions:read,channels:history,im:history,chat:write
227+
228+
# python async_oauth_app.py
229+
# or
230+
# uvicorn async_oauth_app:api --reload --port 3000 --log-level warning

samples/sqlalchemy/oauth_app.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import logging
2+
3+
logging.basicConfig(level=logging.DEBUG)
4+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
5+
6+
import os
7+
from slack_bolt import App
8+
from slack_bolt.adapter.flask import SlackRequestHandler
9+
from slack_bolt.oauth.oauth_settings import OAuthSettings
10+
from slack_sdk.oauth.installation_store.sqlalchemy import SQLAlchemyInstallationStore
11+
from slack_sdk.oauth.state_store.sqlalchemy import SQLAlchemyOAuthStateStore
12+
13+
import sqlalchemy
14+
from sqlalchemy.engine import Engine
15+
16+
database_url = "sqlite:///slackapp.db"
17+
# database_url = "postgresql://localhost/slackapp" # pip install psycopg2
18+
19+
logger = logging.getLogger(__name__)
20+
client_id, client_secret, signing_secret = (
21+
os.environ["SLACK_CLIENT_ID"],
22+
os.environ["SLACK_CLIENT_SECRET"],
23+
os.environ["SLACK_SIGNING_SECRET"],
24+
)
25+
26+
engine: Engine = sqlalchemy.create_engine(database_url)
27+
installation_store = SQLAlchemyInstallationStore(
28+
client_id=client_id, engine=engine, logger=logger,
29+
)
30+
oauth_state_store = SQLAlchemyOAuthStateStore(
31+
expiration_seconds=120, engine=engine, logger=logger,
32+
)
33+
34+
try:
35+
engine.execute("select count(*) from bots")
36+
except Exception as e:
37+
installation_store.metadata.create_all(engine)
38+
oauth_state_store.metadata.create_all(engine)
39+
40+
app = App(
41+
logger=logger,
42+
signing_secret=signing_secret,
43+
installation_store=installation_store,
44+
oauth_settings=OAuthSettings(
45+
client_id=client_id,
46+
client_secret=client_secret,
47+
state_store=oauth_state_store,
48+
),
49+
)
50+
51+
52+
@app.event("app_mention")
53+
def handle_command(say):
54+
say("Hi!")
55+
56+
57+
from flask import Flask, request
58+
59+
flask_app = Flask(__name__)
60+
handler = SlackRequestHandler(app)
61+
62+
63+
@flask_app.route("/slack/events", methods=["POST"])
64+
def slack_events():
65+
return handler.handle(request)
66+
67+
68+
@flask_app.route("/slack/install", methods=["GET"])
69+
def install():
70+
return handler.handle(request)
71+
72+
73+
@flask_app.route("/slack/oauth_redirect", methods=["GET"])
74+
def oauth_redirect():
75+
return handler.handle(request)
76+
77+
78+
# pip install -r requirements.txt
79+
80+
# # -- OAuth flow -- #
81+
# export SLACK_SIGNING_SECRET=***
82+
# export SLACK_BOT_TOKEN=xoxb-***
83+
# export SLACK_CLIENT_ID=111.111
84+
# export SLACK_CLIENT_SECRET=***
85+
# export SLACK_SCOPES=app_mentions:read,chat:write
86+
87+
# FLASK_APP=oauth_app.py FLASK_ENV=development flask run -p 3000
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
slack_bolt>=0.6
2+
Flask>1
3+
SQLAlchemy
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
slack_bolt>=0.6
2+
sanic>=20,<21
3+
uvicorn<1
4+
databases[sqlite]

0 commit comments

Comments
 (0)