Skip to content

Commit

Permalink
Refactor methods for slack and add doc-strings, typing
Browse files Browse the repository at this point in the history
  • Loading branch information
sunank200 committed Oct 30, 2023
1 parent b046f91 commit 666ae07
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 24 deletions.
27 changes: 25 additions & 2 deletions api/ask_astro/slack/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
"Generates the slack app and the slack app handler."
"""
Generates the Slack app and the Slack app handler.
This module sets up the Slack app's OAuth settings and creates an instance
of the Slack app and its handler.
.. note::
**Scopes Required for Slack:**
- `commands`: Add shortcuts and/or slash commands.
- `app_mentions:read`: Read messages that directly mention the app in conversations.
- `channels:read`: View basic information about public channels in the workspace.
- `channels:history`: View messages and other content in public channels.
- `groups:read`: View basic information about private channels.
- `groups:history`: View messages and other content in private channels.
- `chat:write`: Send messages as the app.
- `reactions:read`: View emoji reactions and their associated messages in channels and conversations.
- `reactions:write`: Add and remove emoji reactions to/from messages.
- `users:read`: View people in the workspace.
- `users:read.email`: View email addresses of people in the workspace.
- `team:read`: View name, email domain, and icon for the workspace.
- `im:history`: View messages and other content in direct messages.
- `mpim:history`: View messages and other content in group direct messages.
- `files:read`: View files shared in channels and conversations the app has access to.
"""

from ask_astro.config import FirestoreCollections, SlackAppConfig
from ask_astro.stores.installation_store import AsyncFirestoreInstallationStore
Expand Down Expand Up @@ -36,7 +60,6 @@
),
)


slack_app = AsyncApp(
signing_secret=SlackAppConfig.signing_secret,
oauth_settings=oauth_settings,
Expand Down
19 changes: 17 additions & 2 deletions api/ask_astro/slack/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from __future__ import annotations

import json
import re
from typing import Any

import jinja2


def markdown_to_slack(message: str):
def markdown_to_slack(message: str) -> str:
"""
Convert markdown formatted text to Slack's message format.
:param message: A string containing markdown formatted text.
"""

regexp_replacements = (
(re.compile("^- ", flags=re.M), "• "),
(re.compile("^ - ", flags=re.M), " ◦ "),
Expand All @@ -22,6 +31,12 @@ def markdown_to_slack(message: str):
return message


def get_blocks(block: str, **kwargs):
def get_blocks(block: str, **kwargs: Any) -> list[dict[str, Any]]:
"""
Retrieve a list of Slack blocks by rendering a Jinja2 template.
:param block: Name of the Jinja2 template to render.
:param kwargs: Arguments to be passed to the Jinja2 template.
"""
env = jinja2.Environment(loader=jinja2.FileSystemLoader("ask_astro/templates"), autoescape=True)
return json.loads(env.get_template(block).render(kwargs))["blocks"]
2 changes: 1 addition & 1 deletion api/ask_astro/stores/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"Re-exports stores for easier importing."
"""Re-exports stores for easier importing."""
84 changes: 67 additions & 17 deletions api/ask_astro/stores/installation_store.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
from logging import Logger
from typing import Optional

import google.cloud.firestore
from slack_sdk.oauth.installation_store.async_installation_store import (
Expand All @@ -11,14 +12,24 @@


class AsyncFirestoreInstallationStore(AsyncInstallationStore):
"""Asynchronous Firestore-backed installation store for Slack."""

def __init__(
self,
*,
collection: str,
historical_data_enabled: bool = True,
client_id: Optional[str] = None,
client_id: str | None = None,
logger: Logger = logging.getLogger(__name__),
):
"""
Initialize the AsyncFirestoreInstallationStore.
:param collection: The Firestore collection name.
:param historical_data_enabled: Store historical data if True. Default is True.
:param client_id: Optional Slack client ID.
:param logger: Logger instance.
"""
firestore_client = google.cloud.firestore.AsyncClient()
self.fp = firestore_client.field_path
self.collection = firestore_client.collection(collection)
Expand All @@ -28,11 +39,17 @@ def __init__(

@property
def logger(self) -> Logger:
"""Lazy-loaded logger property."""
if self._logger is None:
self._logger = logging.getLogger(__name__)
return self._logger

async def async_save(self, installation: Installation):
async def async_save(self, installation: Installation) -> None:
"""
Save installation data to Firestore.
:param installation: The installation data to save.
"""
none = "none"
e_id = installation.enterprise_id or none
t_id = installation.team_id or none
Expand Down Expand Up @@ -74,7 +91,12 @@ async def async_save(self, installation: Installation):
{self.fp("installer", u_id, "latest"): entity},
)

async def async_save_bot(self, bot: Bot):
async def async_save_bot(self, bot: Bot) -> None:
"""
Save bot data to Firestore.
:param bot: The bot data to save.
"""
none = "none"
e_id = bot.enterprise_id or none
t_id = bot.team_id or none
Expand All @@ -99,10 +121,17 @@ async def async_save_bot(self, bot: Bot):
async def async_find_bot(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
is_enterprise_install: Optional[bool] = False,
) -> Optional[Bot]:
enterprise_id: str | None,
team_id: str | None,
is_enterprise_install: bool | None = False,
) -> Bot | None:
"""
Find bot data from Firestore.
:param enterprise_id: The enterprise ID.
:param team_id: The team ID.
:param is_enterprise_install: Whether the installation is an enterprise installation.
"""
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
Expand All @@ -121,11 +150,19 @@ async def async_find_bot(
async def async_find_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
is_enterprise_install: Optional[bool] = False,
) -> Optional[Installation]:
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
is_enterprise_install: bool | None = False,
) -> Installation | None:
"""
Find installation data from Firestore.
:param enterprise_id: The enterprise ID.
:param team_id: The team ID.
:param user_id: The user ID.
:param is_enterprise_install: Whether the installation is an enterprise installation.
"""
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
Expand Down Expand Up @@ -173,7 +210,13 @@ async def async_find_installation(
self.logger.debug(message)
return None

async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
async def async_delete_bot(self, *, enterprise_id: str | None, team_id: str | None) -> None:
"""
Delete bot data from Firestore.
:param enterprise_id: The enterprise ID.
:param team_id: The team ID.
"""
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
Expand All @@ -183,10 +226,17 @@ async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optio
async def async_delete_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
) -> None:
"""
Delete installation data from Firestore.
:param enterprise_id: The enterprise ID.
:param team_id: The team ID.
:param user_id: The user ID.
"""
none = "none"
e_id = enterprise_id or none
t_id = team_id or none
Expand Down
22 changes: 20 additions & 2 deletions api/ask_astro/stores/oauth_state_store.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
from __future__ import annotations

import datetime
import logging
from datetime import timedelta
from logging import Logger
from typing import Optional

import google.cloud.firestore
from slack_sdk.oauth.state_store.async_state_store import AsyncOAuthStateStore


class AsyncFirestoreOAuthStateStore(AsyncOAuthStateStore):
"""An async state store backed by Firestore for Slack OAuth flows."""

def __init__(
self,
*,
collection: str,
expiration_seconds: int,
client_id: Optional[str] = None,
client_id: str | None = None,
logger: Logger = logging.getLogger(__name__),
):
"""
Initialize the state store with given parameters.
:param collection: Firestore collection name.
:param expiration_seconds: Duration in seconds before a state becomes expired.
:param client_id: The client ID for Slack OAuth. Default is None.
:param logger: Logger instance. Defaults to the module's logger.
"""
firestore_client = google.cloud.firestore.AsyncClient()
self.collection = firestore_client.collection(collection)
self.expiration_seconds = expiration_seconds
Expand All @@ -26,16 +37,23 @@ def __init__(

@property
def logger(self) -> Logger:
"""Logger property. If `_logger` is None, it initializes a new logger."""
if self._logger is None:
self._logger = logging.getLogger(__name__)
return self._logger

async def async_issue(self, *args, **kwargs) -> str:
"""Issue a new OAuth state and store it in Firestore."""
doc_ref = self.collection.document()
await doc_ref.set({"timestamp": datetime.datetime.now().astimezone()})
return doc_ref.id

async def async_consume(self, state: str) -> bool:
"""
Consume the OAuth state by verifying its validity.
:param state: The state string to verify.
"""
doc_ref = self.collection.document(state)
created = (await doc_ref.get()).get("timestamp")

Expand Down

0 comments on commit 666ae07

Please sign in to comment.