|
16 | 16 | ) |
17 | 17 | from sqlalchemy.engine import Engine |
18 | 18 | from sqlalchemy.sql.sqltypes import Boolean |
19 | | - |
| 19 | +from sqlalchemy.ext.asyncio import AsyncEngine |
20 | 20 | from slack_sdk.oauth.installation_store.installation_store import InstallationStore |
21 | 21 | from slack_sdk.oauth.installation_store.models.bot import Bot |
22 | 22 | from slack_sdk.oauth.installation_store.models.installation import Installation |
| 23 | +from slack_sdk.oauth.installation_store.async_installation_store import ( |
| 24 | + AsyncInstallationStore, |
| 25 | +) |
23 | 26 |
|
24 | 27 |
|
25 | 28 | class SQLAlchemyInstallationStore(InstallationStore): |
@@ -219,21 +222,7 @@ def find_bot( |
219 | 222 | with self.engine.connect() as conn: |
220 | 223 | result: object = conn.execute(query) |
221 | 224 | for row in result.mappings(): # type: ignore[attr-defined] |
222 | | - return Bot( |
223 | | - app_id=row["app_id"], |
224 | | - enterprise_id=row["enterprise_id"], |
225 | | - enterprise_name=row["enterprise_name"], |
226 | | - team_id=row["team_id"], |
227 | | - team_name=row["team_name"], |
228 | | - bot_token=row["bot_token"], |
229 | | - bot_id=row["bot_id"], |
230 | | - bot_user_id=row["bot_user_id"], |
231 | | - bot_scopes=row["bot_scopes"], |
232 | | - bot_refresh_token=row["bot_refresh_token"], |
233 | | - bot_token_expires_at=row["bot_token_expires_at"], |
234 | | - is_enterprise_install=row["is_enterprise_install"], |
235 | | - installed_at=row["installed_at"], |
236 | | - ) |
| 225 | + return self.build_bot_entity(row) |
237 | 226 | return None |
238 | 227 |
|
239 | 228 | def find_installation( |
@@ -267,33 +256,7 @@ def find_installation( |
267 | 256 | with self.engine.connect() as conn: |
268 | 257 | result: object = conn.execute(query) |
269 | 258 | for row in result.mappings(): # type: ignore[attr-defined] |
270 | | - installation = Installation( |
271 | | - app_id=row["app_id"], |
272 | | - enterprise_id=row["enterprise_id"], |
273 | | - enterprise_name=row["enterprise_name"], |
274 | | - enterprise_url=row["enterprise_url"], |
275 | | - team_id=row["team_id"], |
276 | | - team_name=row["team_name"], |
277 | | - bot_token=row["bot_token"], |
278 | | - bot_id=row["bot_id"], |
279 | | - bot_user_id=row["bot_user_id"], |
280 | | - bot_scopes=row["bot_scopes"], |
281 | | - bot_refresh_token=row["bot_refresh_token"], |
282 | | - bot_token_expires_at=row["bot_token_expires_at"], |
283 | | - user_id=row["user_id"], |
284 | | - user_token=row["user_token"], |
285 | | - user_scopes=row["user_scopes"], |
286 | | - user_refresh_token=row["user_refresh_token"], |
287 | | - user_token_expires_at=row["user_token_expires_at"], |
288 | | - # Only the incoming webhook issued in the latest installation is set in this logic |
289 | | - incoming_webhook_url=row["incoming_webhook_url"], |
290 | | - incoming_webhook_channel=row["incoming_webhook_channel"], |
291 | | - incoming_webhook_channel_id=row["incoming_webhook_channel_id"], |
292 | | - incoming_webhook_configuration_url=row["incoming_webhook_configuration_url"], |
293 | | - is_enterprise_install=row["is_enterprise_install"], |
294 | | - token_type=row["token_type"], |
295 | | - installed_at=row["installed_at"], |
296 | | - ) |
| 259 | + installation = self.build_installation_entity(row) |
297 | 260 |
|
298 | 261 | has_user_installation = user_id is not None and installation is not None |
299 | 262 | no_bot_token_installation = installation is not None and installation.bot_token is None |
@@ -362,3 +325,287 @@ def delete_installation( |
362 | 325 | ) |
363 | 326 | ) |
364 | 327 | conn.execute(deletion) |
| 328 | + |
| 329 | + @classmethod |
| 330 | + def build_installation_entity(cls, row) -> Installation: |
| 331 | + return Installation( |
| 332 | + app_id=row["app_id"], |
| 333 | + enterprise_id=row["enterprise_id"], |
| 334 | + enterprise_name=row["enterprise_name"], |
| 335 | + enterprise_url=row["enterprise_url"], |
| 336 | + team_id=row["team_id"], |
| 337 | + team_name=row["team_name"], |
| 338 | + bot_token=row["bot_token"], |
| 339 | + bot_id=row["bot_id"], |
| 340 | + bot_user_id=row["bot_user_id"], |
| 341 | + bot_scopes=row["bot_scopes"], |
| 342 | + bot_refresh_token=row["bot_refresh_token"], |
| 343 | + bot_token_expires_at=row["bot_token_expires_at"], |
| 344 | + user_id=row["user_id"], |
| 345 | + user_token=row["user_token"], |
| 346 | + user_scopes=row["user_scopes"], |
| 347 | + user_refresh_token=row["user_refresh_token"], |
| 348 | + user_token_expires_at=row["user_token_expires_at"], |
| 349 | + # Only the incoming webhook issued in the latest installation is set in this logic |
| 350 | + incoming_webhook_url=row["incoming_webhook_url"], |
| 351 | + incoming_webhook_channel=row["incoming_webhook_channel"], |
| 352 | + incoming_webhook_channel_id=row["incoming_webhook_channel_id"], |
| 353 | + incoming_webhook_configuration_url=row["incoming_webhook_configuration_url"], |
| 354 | + is_enterprise_install=row["is_enterprise_install"], |
| 355 | + token_type=row["token_type"], |
| 356 | + installed_at=row["installed_at"], |
| 357 | + ) |
| 358 | + |
| 359 | + @classmethod |
| 360 | + def build_bot_entity(cls, row) -> Bot: |
| 361 | + return Bot( |
| 362 | + app_id=row["app_id"], |
| 363 | + enterprise_id=row["enterprise_id"], |
| 364 | + enterprise_name=row["enterprise_name"], |
| 365 | + team_id=row["team_id"], |
| 366 | + team_name=row["team_name"], |
| 367 | + bot_token=row["bot_token"], |
| 368 | + bot_id=row["bot_id"], |
| 369 | + bot_user_id=row["bot_user_id"], |
| 370 | + bot_scopes=row["bot_scopes"], |
| 371 | + bot_refresh_token=row["bot_refresh_token"], |
| 372 | + bot_token_expires_at=row["bot_token_expires_at"], |
| 373 | + is_enterprise_install=row["is_enterprise_install"], |
| 374 | + installed_at=row["installed_at"], |
| 375 | + ) |
| 376 | + |
| 377 | + |
| 378 | +class AsyncSQLAlchemyInstallationStore(AsyncInstallationStore): |
| 379 | + default_bots_table_name: str = "slack_bots" |
| 380 | + default_installations_table_name: str = "slack_installations" |
| 381 | + |
| 382 | + client_id: str |
| 383 | + engine: AsyncEngine |
| 384 | + metadata: MetaData |
| 385 | + installations: Table |
| 386 | + |
| 387 | + def __init__( |
| 388 | + self, |
| 389 | + client_id: str, |
| 390 | + engine: AsyncEngine, |
| 391 | + bots_table_name: str = default_bots_table_name, |
| 392 | + installations_table_name: str = default_installations_table_name, |
| 393 | + logger: Logger = logging.getLogger(__name__), |
| 394 | + ): |
| 395 | + self.metadata = sqlalchemy.MetaData() |
| 396 | + self.bots = self.build_bots_table(metadata=self.metadata, table_name=bots_table_name) |
| 397 | + self.installations = self.build_installations_table(metadata=self.metadata, table_name=installations_table_name) |
| 398 | + self.client_id = client_id |
| 399 | + self._logger = logger |
| 400 | + self.engine = engine |
| 401 | + |
| 402 | + @classmethod |
| 403 | + def build_installations_table(cls, metadata: MetaData, table_name: str) -> Table: |
| 404 | + return SQLAlchemyInstallationStore.build_installations_table(metadata, table_name) |
| 405 | + |
| 406 | + @classmethod |
| 407 | + def build_bots_table(cls, metadata: MetaData, table_name: str) -> Table: |
| 408 | + return SQLAlchemyInstallationStore.build_bots_table(metadata, table_name) |
| 409 | + |
| 410 | + async def create_tables(self): |
| 411 | + async with self.engine.begin() as conn: |
| 412 | + await conn.run_sync(self.metadata.create_all) |
| 413 | + |
| 414 | + @property |
| 415 | + def logger(self) -> Logger: |
| 416 | + return self._logger |
| 417 | + |
| 418 | + async def async_save(self, installation: Installation): |
| 419 | + async with self.engine.begin() as conn: |
| 420 | + i = installation.to_dict() |
| 421 | + i["client_id"] = self.client_id |
| 422 | + |
| 423 | + i_column = self.installations.c |
| 424 | + installations_rows = await conn.execute( |
| 425 | + sqlalchemy.select(i_column.id) |
| 426 | + .where( |
| 427 | + and_( |
| 428 | + i_column.client_id == self.client_id, |
| 429 | + i_column.enterprise_id == installation.enterprise_id, |
| 430 | + i_column.team_id == installation.team_id, |
| 431 | + i_column.installed_at == i.get("installed_at"), |
| 432 | + ) |
| 433 | + ) |
| 434 | + .limit(1) |
| 435 | + ) |
| 436 | + installations_row_id: Optional[str] = None |
| 437 | + for row in installations_rows.mappings(): |
| 438 | + installations_row_id = row["id"] |
| 439 | + if installations_row_id is None: |
| 440 | + await conn.execute(self.installations.insert(), i) |
| 441 | + else: |
| 442 | + update_statement = self.installations.update().where(i_column.id == installations_row_id).values(**i) |
| 443 | + await conn.execute(update_statement, i) |
| 444 | + |
| 445 | + # bots |
| 446 | + await self.async_save_bot(installation.to_bot()) |
| 447 | + |
| 448 | + async def async_save_bot(self, bot: Bot): |
| 449 | + async with self.engine.begin() as conn: |
| 450 | + # bots |
| 451 | + b = bot.to_dict() |
| 452 | + b["client_id"] = self.client_id |
| 453 | + |
| 454 | + b_column = self.bots.c |
| 455 | + bots_rows = await conn.execute( |
| 456 | + sqlalchemy.select(b_column.id) |
| 457 | + .where( |
| 458 | + and_( |
| 459 | + b_column.client_id == self.client_id, |
| 460 | + b_column.enterprise_id == bot.enterprise_id, |
| 461 | + b_column.team_id == bot.team_id, |
| 462 | + b_column.installed_at == b.get("installed_at"), |
| 463 | + ) |
| 464 | + ) |
| 465 | + .limit(1) |
| 466 | + ) |
| 467 | + bots_row_id: Optional[str] = None |
| 468 | + for row in bots_rows.mappings(): |
| 469 | + bots_row_id = row["id"] |
| 470 | + if bots_row_id is None: |
| 471 | + await conn.execute(self.bots.insert(), b) |
| 472 | + else: |
| 473 | + update_statement = self.bots.update().where(b_column.id == bots_row_id).values(**b) |
| 474 | + await conn.execute(update_statement, b) |
| 475 | + |
| 476 | + async def async_find_bot( |
| 477 | + self, |
| 478 | + *, |
| 479 | + enterprise_id: Optional[str], |
| 480 | + team_id: Optional[str], |
| 481 | + is_enterprise_install: Optional[bool] = False, |
| 482 | + ) -> Optional[Bot]: |
| 483 | + if is_enterprise_install or team_id is None: |
| 484 | + team_id = None |
| 485 | + |
| 486 | + c = self.bots.c |
| 487 | + query = ( |
| 488 | + self.bots.select() |
| 489 | + .where( |
| 490 | + and_( |
| 491 | + c.client_id == self.client_id, |
| 492 | + c.enterprise_id == enterprise_id, |
| 493 | + c.team_id == team_id, |
| 494 | + c.bot_token.is_not(None), # the latest one that has a bot token |
| 495 | + ) |
| 496 | + ) |
| 497 | + .order_by(desc(c.installed_at)) |
| 498 | + .limit(1) |
| 499 | + ) |
| 500 | + |
| 501 | + async with self.engine.connect() as conn: |
| 502 | + result: object = await conn.execute(query) |
| 503 | + for row in result.mappings(): # type: ignore[attr-defined] |
| 504 | + return SQLAlchemyInstallationStore.build_bot_entity(row) |
| 505 | + return None |
| 506 | + |
| 507 | + async def async_find_installation( |
| 508 | + self, |
| 509 | + *, |
| 510 | + enterprise_id: Optional[str], |
| 511 | + team_id: Optional[str], |
| 512 | + user_id: Optional[str] = None, |
| 513 | + is_enterprise_install: Optional[bool] = False, |
| 514 | + ) -> Optional[Installation]: |
| 515 | + if is_enterprise_install or team_id is None: |
| 516 | + team_id = None |
| 517 | + |
| 518 | + c = self.installations.c |
| 519 | + where_clause = and_( |
| 520 | + c.client_id == self.client_id, |
| 521 | + c.enterprise_id == enterprise_id, |
| 522 | + c.team_id == team_id, |
| 523 | + ) |
| 524 | + if user_id is not None: |
| 525 | + where_clause = and_( |
| 526 | + c.client_id == self.client_id, |
| 527 | + c.enterprise_id == enterprise_id, |
| 528 | + c.team_id == team_id, |
| 529 | + c.user_id == user_id, |
| 530 | + ) |
| 531 | + |
| 532 | + query = self.installations.select().where(where_clause).order_by(desc(c.installed_at)).limit(1) |
| 533 | + |
| 534 | + installation: Optional[Installation] = None |
| 535 | + async with self.engine.connect() as conn: |
| 536 | + result: object = await conn.execute(query) |
| 537 | + for row in result.mappings(): # type: ignore[attr-defined] |
| 538 | + installation = SQLAlchemyInstallationStore.build_installation_entity(row) |
| 539 | + |
| 540 | + has_user_installation = user_id is not None and installation is not None |
| 541 | + no_bot_token_installation = installation is not None and installation.bot_token is None |
| 542 | + should_find_bot_installation = has_user_installation or no_bot_token_installation |
| 543 | + if should_find_bot_installation: |
| 544 | + # Retrieve the latest bot token, just in case |
| 545 | + # See also: https://github.com/slackapi/bolt-python/issues/664 |
| 546 | + latest_bot_installation = await self.async_find_bot( |
| 547 | + enterprise_id=enterprise_id, |
| 548 | + team_id=team_id, |
| 549 | + is_enterprise_install=is_enterprise_install, |
| 550 | + ) |
| 551 | + if ( |
| 552 | + latest_bot_installation is not None |
| 553 | + and installation is not None |
| 554 | + and installation.bot_token != latest_bot_installation.bot_token |
| 555 | + ): |
| 556 | + installation.bot_id = latest_bot_installation.bot_id |
| 557 | + installation.bot_user_id = latest_bot_installation.bot_user_id |
| 558 | + installation.bot_token = latest_bot_installation.bot_token |
| 559 | + installation.bot_scopes = latest_bot_installation.bot_scopes |
| 560 | + installation.bot_refresh_token = latest_bot_installation.bot_refresh_token |
| 561 | + installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at |
| 562 | + |
| 563 | + return installation |
| 564 | + |
| 565 | + async def async_delete_bot( |
| 566 | + self, |
| 567 | + *, |
| 568 | + enterprise_id: Optional[str], |
| 569 | + team_id: Optional[str], |
| 570 | + ) -> None: |
| 571 | + table = self.bots |
| 572 | + c = table.c |
| 573 | + async with self.engine.begin() as conn: |
| 574 | + deletion = table.delete().where( |
| 575 | + and_( |
| 576 | + c.client_id == self.client_id, |
| 577 | + c.enterprise_id == enterprise_id, |
| 578 | + c.team_id == team_id, |
| 579 | + ) |
| 580 | + ) |
| 581 | + await conn.execute(deletion) |
| 582 | + |
| 583 | + async def async_delete_installation( |
| 584 | + self, |
| 585 | + *, |
| 586 | + enterprise_id: Optional[str], |
| 587 | + team_id: Optional[str], |
| 588 | + user_id: Optional[str] = None, |
| 589 | + ) -> None: |
| 590 | + table = self.installations |
| 591 | + c = table.c |
| 592 | + async with self.engine.begin() as conn: |
| 593 | + if user_id is not None: |
| 594 | + deletion = table.delete().where( |
| 595 | + and_( |
| 596 | + c.client_id == self.client_id, |
| 597 | + c.enterprise_id == enterprise_id, |
| 598 | + c.team_id == team_id, |
| 599 | + c.user_id == user_id, |
| 600 | + ) |
| 601 | + ) |
| 602 | + await conn.execute(deletion) |
| 603 | + else: |
| 604 | + deletion = table.delete().where( |
| 605 | + and_( |
| 606 | + c.client_id == self.client_id, |
| 607 | + c.enterprise_id == enterprise_id, |
| 608 | + c.team_id == team_id, |
| 609 | + ) |
| 610 | + ) |
| 611 | + await conn.execute(deletion) |
0 commit comments