From 1fd109c541ba40750c2410b482367a730f696899 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Mon, 30 Sep 2024 16:59:21 -0400 Subject: [PATCH] Add TemplateService This service carries out functionality on individual templates, like creating the modal for a user to specify template variables and for rendering out the template. This plums the block action triggered by a template selection menu through the TemplateService and into the TemplateVariablesModal to create the modal. --- src/templatebot/factory.py | 12 +- src/templatebot/services/slackblockactions.py | 149 ++++++++++-------- src/templatebot/services/template.py | 122 ++++++++++++++ 3 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 src/templatebot/services/template.py diff --git a/src/templatebot/factory.py b/src/templatebot/factory.py index a91502c..a759a7b 100644 --- a/src/templatebot/factory.py +++ b/src/templatebot/factory.py @@ -10,6 +10,7 @@ from templatebot.services.slackblockactions import SlackBlockActionsService from templatebot.services.slackmessage import SlackMessageService from templatebot.services.slackview import SlackViewService +from templatebot.services.template import TemplateService from templatebot.services.templaterepo import TemplateRepoService from templatebot.storage.repo import RepoManager from templatebot.storage.slack import SlackWebApiClient @@ -94,7 +95,10 @@ def create_slack_message_service(self) -> SlackMessageService: def create_slack_block_actions_service(self) -> SlackBlockActionsService: """Create a new Slack block actions handling service.""" return SlackBlockActionsService( - logger=self._logger, slack_client=self.create_slack_web_client() + logger=self._logger, + slack_client=self.create_slack_web_client(), + repo_manager=self._process_context.repo_manager, + template_service=self.create_template_service(), ) def create_slack_view_service(self) -> SlackViewService: @@ -110,3 +114,9 @@ def create_template_repo_service(self) -> TemplateRepoService: repo_manager=self._process_context.repo_manager, slack_client=self.create_slack_web_client(), ) + + def create_template_service(self) -> TemplateService: + """Create a new template service.""" + return TemplateService( + logger=self._logger, slack_client=self.create_slack_web_client() + ) diff --git a/src/templatebot/services/slackblockactions.py b/src/templatebot/services/slackblockactions.py index 23f901f..f194e3e 100644 --- a/src/templatebot/services/slackblockactions.py +++ b/src/templatebot/services/slackblockactions.py @@ -8,20 +8,16 @@ SlackStaticSelectAction, ) from structlog.stdlib import BoundLogger +from templatekit.repo import FileTemplate, ProjectTemplate -from templatebot.constants import SELECT_PROJECT_TEMPLATE_ACTION -from templatebot.storage.slack import SlackWebApiClient -from templatebot.storage.slack._models import SlackChatUpdateMessageRequest -from templatebot.storage.slack.blockkit import ( - SlackInputBlock, - SlackMrkdwnTextObject, - SlackOptionObject, - SlackPlainTextInputElement, - SlackPlainTextObject, - SlackSectionBlock, - SlackStaticSelectElement, +from templatebot.config import config +from templatebot.constants import ( + SELECT_FILE_TEMPLATE_ACTION, + SELECT_PROJECT_TEMPLATE_ACTION, ) -from templatebot.storage.slack.views import SlackModalView +from templatebot.services.template import TemplateService +from templatebot.storage.repo import RepoManager +from templatebot.storage.slack import SlackWebApiClient __all__ = ["SlackBlockActionsService"] @@ -30,10 +26,16 @@ class SlackBlockActionsService: """A service for processing Slack block actions.""" def __init__( - self, logger: BoundLogger, slack_client: SlackWebApiClient + self, + logger: BoundLogger, + slack_client: SlackWebApiClient, + template_service: TemplateService, + repo_manager: RepoManager, ) -> None: self._logger = logger self._slack_client = slack_client + self._template_service = template_service + self._repo_manager = repo_manager async def handle_block_actions( self, payload: SquarebotSlackBlockActionsValue @@ -44,6 +46,10 @@ async def handle_block_actions( await self.handle_project_template_selection( action=action, payload=payload ) + elif action.action_id == SELECT_FILE_TEMPLATE_ACTION: + await self.handle_file_template_selection( + action=action, payload=payload + ) async def handle_project_template_selection( self, @@ -71,61 +77,70 @@ async def handle_project_template_selection( raise ValueError("No message in payload") original_message_ts = payload.message.ts - updated_messsage = SlackChatUpdateMessageRequest( - channel=original_message_channel, - ts=original_message_ts, - text=( - f"We'll create a project with the {selected_option.text.text} " - "template" - ), - ) - await self._slack_client.update_message(updated_messsage) + git_ref = "main" - demo_section = SlackSectionBlock( - text=SlackMrkdwnTextObject( - text=f"Let's create a {selected_option.text.text} project." - ), - ) - demo_select_input = SlackInputBlock( - label=SlackPlainTextObject(text="License"), - element=SlackStaticSelectElement( - placeholder=SlackPlainTextObject(text="Choose a license…"), - action_id="select_license", - options=[ - SlackOptionObject( - text=SlackPlainTextObject(text="MIT"), - value="mit", - ), - SlackOptionObject( - text=SlackPlainTextObject(text="GPLv3"), - value="gplv3", - ), - ], - ), - block_id="license", - hint=SlackPlainTextObject(text="MIT is preferred."), - ) - demo_text_input = SlackInputBlock( - label=SlackPlainTextObject(text="Project name"), - element=SlackPlainTextInputElement( - placeholder=SlackPlainTextObject(text="Enter a project name…"), - action_id="project_name", - min_length=3, - ), - block_id="project_name", - ) - modal = SlackModalView( - title=SlackPlainTextObject(text="Set up your project"), - blocks=[demo_section, demo_select_input, demo_text_input], - submit=SlackPlainTextObject(text="Create project"), - close=SlackPlainTextObject(text="Cancel"), + template = self._repo_manager.get_repo(gitref=git_ref)[ + selected_option.value + ] + if not isinstance(template, ProjectTemplate): + raise TypeError( + f"Expected {selected_option.value} template to be a " + f"ProjectTemplate, but got {type(template)}" + ) + + await self._template_service.show_project_template_modal( + user_id=payload.user.id, + trigger_id=payload.trigger_id, + message_ts=original_message_ts, + channel_id=original_message_channel, + template=template, + git_ref=git_ref, + repo_url=str(config.template_repo_url), ) - response = await self._slack_client.open_view( - trigger_id=payload.trigger_id, view=modal + + async def handle_file_template_selection( + self, + *, + action: SlackBlockActionBase, + payload: SquarebotSlackBlockActionsValue, + ) -> None: + """Handle a file template selection.""" + if not isinstance(action, SlackStaticSelectAction): + raise TypeError( + f"Expected action for {SELECT_FILE_TEMPLATE_ACTION} to be " + f"a SlackStaticSelectAction, but got {type(action)}" + ) + selected_option = action.selected_option + self._logger.debug( + "Selected file template", + value=selected_option.value, + text=selected_option.text.text, ) - if not response["ok"]: - self._logger.error( - "Failed to open view", - response=response, - payload=payload.model_dump(mode="json"), + + if not payload.channel: + raise ValueError("No channel in payload") + original_message_channel = payload.channel.id + if not payload.message: + raise ValueError("No message in payload") + original_message_ts = payload.message.ts + + git_ref = "main" + + template = self._repo_manager.get_repo(gitref=git_ref)[ + selected_option.value + ] + if not isinstance(template, FileTemplate): + raise TypeError( + f"Expected {selected_option.value} template to be a " + f"ProjectTemplate, but got {type(template)}" ) + + await self._template_service.show_file_template_modal( + user_id=payload.user.id, + trigger_id=payload.trigger_id, + message_ts=original_message_ts, + channel_id=original_message_channel, + template=template, + git_ref=git_ref, + repo_url=str(config.template_repo_url), + ) diff --git a/src/templatebot/services/template.py b/src/templatebot/services/template.py new file mode 100644 index 0000000..b20fe31 --- /dev/null +++ b/src/templatebot/services/template.py @@ -0,0 +1,122 @@ +"""Template service.""" + +from __future__ import annotations + +from structlog.stdlib import BoundLogger +from templatekit.repo import FileTemplate, ProjectTemplate + +from templatebot.storage.slack import ( + SlackChatUpdateMessageRequest, + SlackWebApiClient, +) +from templatebot.storage.slack.variablesmodal import TemplateVariablesModal + +__all__ = ["TemplateService"] + + +class TemplateService: + """A service for operating with templates. + + Features include: + + - Having a user configure a template through a Slack modal view + - Rendering a template with user-provided values and running the + configuration of that repository and LSST the Docs services. + """ + + def __init__( + self, *, logger: BoundLogger, slack_client: SlackWebApiClient + ) -> None: + self._logger = logger + self._slack_client = slack_client + + async def show_file_template_modal( + self, + *, + user_id: str, + trigger_id: str, + message_ts: str, + channel_id: str, + template: FileTemplate, + git_ref: str, + repo_url: str, + ) -> None: + """Show a modal for selecting a file template.""" + if len(template.config["dialog_fields"]) == 0: + await self._respond_with_nonconfigurable_content( + template=template, + channel_id=channel_id, + trigger_message_ts=message_ts, + ) + else: + await self._open_template_modal( + template=template, + trigger_id=trigger_id, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=message_ts, + trigger_channel_id=channel_id, + ) + + async def show_project_template_modal( + self, + *, + user_id: str, + trigger_id: str, + message_ts: str, + channel_id: str, + template: ProjectTemplate, + git_ref: str, + repo_url: str, + ) -> None: + """Show a modal for selecting a project template.""" + await self._open_template_modal( + template=template, + trigger_id=trigger_id, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=message_ts, + trigger_channel_id=channel_id, + ) + + async def _open_template_modal( + self, + *, + template: FileTemplate | ProjectTemplate, + trigger_id: str, + git_ref: str, + repo_url: str, + trigger_message_ts: str | None = None, + trigger_channel_id: str | None = None, + ) -> None: + """Open a modal for configuring a template.""" + modal_view = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + await self._slack_client.open_view( + trigger_id=trigger_id, view=modal_view + ) + + async def _respond_with_nonconfigurable_content( + self, + *, + template: FileTemplate, + channel_id: str, + trigger_message_ts: str, + ) -> None: + """Respond with non-configurable content.""" + # TODO(jonathansick): render the template and send it back to the user + await self._slack_client.update_message( + message_update_request=SlackChatUpdateMessageRequest( + channel=channel_id, + ts=trigger_message_ts, + text=( + f"The {template.name} template does not require " + "configuration." + ), + ) + )