diff --git a/.gitignore b/.gitignore index 954bfb3..34c45d5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ tmp.txt .DS_Store logs/ *.db -.pytype/ \ No newline at end of file +.pytype/ +scripts +.slack \ No newline at end of file diff --git a/README.md b/README.md index dfae2fa..55ceca6 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,67 @@ -# Bolt for Python Template App -This is a generic Bolt for Python template app used to build out Slack apps. +# Bolt Python Starter Template + +This app contains a generic Bolt for Python template app used to build out Slack apps on Slack's +[next-generation platform](https://api.slack.com/future). Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). -## Installation -#### Create a Slack App -1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest" -2. Choose the workspace you want to install the application to -3. Copy the contents of [manifest.json](./manifest.json) into the text box that says `*Paste your manifest code here*` (within the JSON tab) and click *Next* -4. Review the configuration and click *Create* -5. Click *Install to Workspace* and *Allow* on the screen that follows. You'll then be redirected to the App Configuration dashboard. +**Guide Outline**: -#### Environment Variables -Before you can run the app, you'll need to store some environment variables. +- [Bolt Python Starter Template ](#bolt-python-starter-template) + - [Supported Workflows](#supported-workflows) + - [Setup](#setup) + - [Install the Slack CLI](#install-the-slack-cli) + - [Clone the Sample App](#clone-the-sample-app) + - [Linting](#linting) + - [Create a Link Trigger](#create-a-link-trigger) + - [Running Your Project Locally](#running-your-project-locally) + - [Project Structure](#project-structure) + - [`manifest.json`](#manifestjson) + - [`/triggers`](#triggers) + - [`slack.json`](#slackjson) + - [Resources](#resources) -1. Open your apps configuration page from this list, click **OAuth & Permissions** in the left hand menu, then copy the Bot User OAuth Token. You will store this in your environment as `SLACK_BOT_TOKEN`. -2. Click ***Basic Information** from the left hand menu and follow the steps in the App-Level Tokens section to create an app-level token with the `connections:write` scope. Copy this token. You will store this in your environment as `SLACK_APP_TOKEN`. +--- -```zsh -# Replace with your app token and bot token -export SLACK_BOT_TOKEN= -export SLACK_APP_TOKEN= -``` +## Supported Workflows + +- **Sample workflow**: Enter details to send a message to a channel + +## Setup + +Before getting started, make sure you have a development workspace where you +have permissions to install apps. If you don’t have one set up, go ahead and +[create one](https://slack.com/create). Also, please note that the workspace +requires any of [the Slack paid plans](https://slack.com/pricing). + +### Install the Slack CLI + +To use this sample, you first need to install and configure the Slack CLI. +Step-by-step instructions can be found in our +[Quickstart Guide](https://api.slack.com/future/quickstart). + +### Clone the Sample App + +Start by cloning this repository: -### Setup Your Local Project ```zsh # Clone this project onto your machine -git clone https://github.com/slackapi/bolt-python-template.git +$ slack create my-app -t slack-samples/bolt-python-starter-template -b future # Change into this project directory -cd bolt-python-starter-template +$ cd my-app # Setup your python virtual environment -python3 -m venv .venv -source .venv/bin/activate - -# Install the dependencies -pip install -r requirements.txt +$ python3 -m venv .venv +$ source .venv/bin/activate -# Start your local server -python3 app.py +# Install the project dependencies +$ pip install -r requirements.txt ``` #### Linting + ```zsh # Run flake8 from root directory for linting flake8 *.py && flake8 listeners/ @@ -52,41 +70,89 @@ flake8 *.py && flake8 listeners/ black . ``` -## Project Structure +## Create a Link Trigger -### `manifest.json` +[Triggers](https://api.slack.com/future/triggers) are what cause workflows to +run. These triggers can be invoked by a user, or automatically as a response to +an event within Slack. -`manifest.json` is a configuration for Slack apps. With a manifest, you can create an app with a pre-defined configuration, or adjust the configuration of an existing app. +A [link trigger](https://api.slack.com/future/triggers/link) is a type of +trigger that generates a **Shortcut URL** which, when posted in a channel or +added as a bookmark, becomes a link. When clicked, the link trigger will run the +associated workflow. -### `app.py` +Link triggers are _unique to each installed version of your app_. This means +that Shortcut URLs will be different across each workspace, as well as between +[locally run](#running-your-project-locally). When creating a trigger, you must select +the Workspace that you'd like to create the trigger in. Each Workspace has a +development version (denoted by `(dev)`), as well as a deployed version. -`app.py` is the entry point for the application and is the file you'll run to start the server. This project aims to keep this file as thin as possible, primarily using it as a way to route inbound requests. +To create a link trigger for the sample workflow, run the following +command: -### `/listeners` - -Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/shortcuts` handles incoming [Shortcuts](https://api.slack.com/interactivity/shortcuts) requests, `/listeners/views` handles [View submissions](https://api.slack.com/reference/interaction-payloads/views#view_submission) and so on. +```zsh +slack trigger create --trigger-def triggers/sample_trigger.json +``` +After selecting a Workspace, the output provided will include the link trigger +Shortcut URL. Copy and paste this URL into a channel as a message, or add it as +a bookmark in a channel of the Workspace you selected. -## App Distribution / OAuth +**Note: this link won't run the workflow until the app is either running locally +or deployed!** Read on to learn how to run your app locally and eventually +deploy it to Slack hosting. -Only implement OAuth if you plan to distribute your application across multiple workspaces. A separate `app-oauth.py` file can be found with relevant OAuth settings. +## Running Your Project Locally -When using OAuth, Slack requires a public URL where it can send requests. In this template app, we've used [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://ngrok.com/docs#getting-started-expose) for setting it up. +While building your app, you can see your changes propagated to your workspace +in real-time with `slack run`. In both the CLI and in Slack, you'll know an app +is the development version if the name has the string `(dev)` appended. -Start `ngrok` to access the app on an external network and create a redirect URL for OAuth. +```zsh +# Run app locally +$ slack run -``` -ngrok http 3000 +⚡️ Bolt app is running! ⚡️ ``` -This output should include a forwarding address for `http` and `https` (we'll use `https`). It should look something like the following: +Once running, click the +[previously created Shortcut URL](#create-a-link-trigger) associated with the +`(dev)` version of your app. This should start a workflow that opens a form used +to send a message to a certain channel! -``` -Forwarding https://3cb89939.ngrok.io -> http://localhost:3000 -``` +To stop running locally, press ` + C` to end the process. -Navigate to **OAuth & Permissions** in your app configuration and click **Add a Redirect URL**. The redirect URL should be set to your `ngrok` forwarding address with the `slack/oauth_redirect` path appended. For example: +## Project Structure -``` -https://3cb89939.ngrok.io/slack/oauth_redirect -``` \ No newline at end of file +### `manifest.json` + +`manifest.json` is a configuration for Slack CLI apps in JSON. This file will +establish all basic configurations for your application, including app name +and description. + +Within the manifest are initializations for [workflows](https://api.slack.com/future/workflows) and [functions](https://api.slack.com/future/functions) are reusable building blocks +of automation that accept inputs, perform calculations, and provide outputs. +Functions can be used independently or as steps in workflows. + +### `/triggers` + +All trigger configuration files live in here - for this example, +`sample_trigger.json` is the trigger config for a trigger that starts the workflow + initialized in `/manifest/manifest.json`. + +### `slack.json` + +Used by the CLI to interact with the project's SDK dependencies. It contains +script hooks that are executed by the CLI and implemented by the SDK. + + +## Resources + +To learn more about developing with the CLI, you can visit the following guides: + +- [Creating a new app with the CLI](https://api.slack.com/future/create) +- [Configuring your app](https://api.slack.com/future/manifest) +- [Developing locally](https://api.slack.com/future/run) + +To view all documentation and guides available, visit the +[Overview page](https://api.slack.com/future/overview). diff --git a/app.py b/app.py index 854f80e..538f77a 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,7 @@ from listeners import register_listeners + # Initialization app = App(token=os.environ.get("SLACK_BOT_TOKEN")) logging.basicConfig(level=logging.DEBUG) diff --git a/listeners/__init__.py b/listeners/__init__.py index f95a68d..22c383d 100644 --- a/listeners/__init__.py +++ b/listeners/__init__.py @@ -4,6 +4,7 @@ from listeners import messages from listeners import shortcuts from listeners import views +from listeners import functions def register_listeners(app): @@ -13,3 +14,4 @@ def register_listeners(app): messages.register(app) shortcuts.register(app) views.register(app) + functions.register(app) diff --git a/listeners/functions/__init__.py b/listeners/functions/__init__.py new file mode 100644 index 0000000..21e8a12 --- /dev/null +++ b/listeners/functions/__init__.py @@ -0,0 +1,7 @@ +from slack_bolt import App + +from .sample_function import sample_function + + +def register(app: App): + app.function("sample_function")(sample_function) diff --git a/listeners/functions/sample_function.py b/listeners/functions/sample_function.py new file mode 100644 index 0000000..5ec5038 --- /dev/null +++ b/listeners/functions/sample_function.py @@ -0,0 +1,12 @@ +from logging import Logger +from slack_bolt import Complete + + +def sample_function(event, complete: Complete, logger: Logger): + try: + message = event["inputs"]["message"] + complete(outputs={"updatedMsg": f":wave: You submitted the following message: \n\n>{message}"}) + except Exception as e: + logger.error(e) + complete(error="Cannot submit the message") + raise e diff --git a/listeners/messages/sample_message.py b/listeners/messages/sample_message.py index 29429a3..9f280e7 100644 --- a/listeners/messages/sample_message.py +++ b/listeners/messages/sample_message.py @@ -1,10 +1,9 @@ from logging import Logger from slack_bolt import BoltContext, Say -from slack_sdk import WebClient -def sample_message_callback(context: BoltContext, client: WebClient, say: Say, logger: Logger): +def sample_message_callback(context: BoltContext, say: Say, logger: Logger): try: greeting = context["matches"][0] say(f"{greeting}, how are you?") diff --git a/manifest.json b/manifest.json index f973e42..aaca8ae 100644 --- a/manifest.json +++ b/manifest.json @@ -1,58 +1,157 @@ { + "$schema": "https://raw.githubusercontent.com/slackapi/manifest-schema/main/manifest.schema.json", "_metadata": { - "major_version": 1, - "minor_version": 1 + "major_version": 2, + "minor_version": 2 }, "display_information": { - "name": "Bolt Template App" + "name": "Bolt Template App" }, "features": { - "app_home": { - "home_tab_enabled": true, - "messages_tab_enabled": false, - "messages_tab_read_only_enabled": true - }, - "bot_user": { - "display_name": "Bolt Template App", - "always_online": false - }, - "shortcuts": [ - { - "name": "Run sample shortcut", - "type": "global", - "callback_id": "sample_shortcut_id", - "description": "Runs a sample shortcut" - } - ], - "slash_commands": [ - { - "command": "/sample-command", - "description": "Runs a sample command", - "should_escape": false - } - ] + "app_home": { + "home_tab_enabled": true, + "messages_tab_enabled": true, + "messages_tab_read_only_enabled": true + }, + "bot_user": { + "display_name": "Bolt Template App", + "always_online": false + }, + "shortcuts": [ + { + "name": "Run sample shortcut", + "type": "global", + "callback_id": "sample_shortcut_id", + "description": "Runs a sample shortcut" + } + ], + "slash_commands": [ + { + "command": "/sample-command", + "description": "Runs a sample command", + "should_escape": false + } + ] }, "oauth_config": { - "scopes": { - "bot": [ - "channels:history", - "chat:write", - "commands" - ] - } + "scopes": { + "bot": [ + "channels:history", + "chat:write", + "commands", + "chat:write.public" + ] + } }, "settings": { - "event_subscriptions": { - "bot_events": [ - "app_home_opened", - "message.channels" - ] + "event_subscriptions": { + "bot_events": [ + "app_home_opened", + "message.channels" + ] + }, + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": true, + "socket_mode_enabled": true, + "token_rotation_enabled": false, + "function_runtime": "remote" + }, + "functions": { + "sample_function": { + "title": "Sample function", + "description": "A sample function", + "input_parameters": { + "properties": { + "message": { + "type": "string", + "description": "Message to be posted" + } + }, + "required": [ + "message" + ] }, - "interactivity": { - "is_enabled": true + "output_parameters": { + "properties": { + "updatedMsg": { + "type": "string", + "description": "Updated message to be posted" + } + }, + "required": [ + "updatedMsg" + ] + } + } + }, + "types": {}, + "workflows": { + "sample_workflow": { + "title": "Sample workflow", + "description": "A sample workflow", + "input_parameters": { + "properties": { + "interactivity": { + "type": "slack#/types/interactivity" + }, + "channel": { + "type": "slack#/types/channel_id" + } + }, + "required": [ + "interactivity" + ] }, - "org_deploy_enabled": false, - "socket_mode_enabled": true, - "token_rotation_enabled": false - } -} \ No newline at end of file + "steps": [ + { + "id": "0", + "function_id": "slack#/functions/open_form", + "inputs": { + "title": "Send message to channel", + "submit_label": "Send message", + "description": "Send a message to a channel", + "interactivity": "{{inputs.interactivity}}", + "fields": { + "elements": [ + { + "name": "message", + "title": "Message", + "type": "string", + "long": true + }, + { + "name": "channel", + "title": "Channel to send message to", + "type": "slack#/types/channel_id", + "default": "{{inputs.channel}}" + } + ], + "required": [ + "channel", + "message" + ] + } + } + }, + { + "id": "1", + "function_id": "#/functions/sample_function", + "inputs": { + "message": "{{steps.0.fields.message}}" + } + }, + { + "id": "2", + "function_id": "slack#/functions/send_message", + "inputs": { + "channel_id": "{{steps.0.fields.channel}}", + "message": "{{steps.1.updatedMsg}}" + } + } + ] + } + }, + "outgoing_domains": [] +} diff --git a/requirements.txt b/requirements.txt index 4ad1d05..3db35e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -slack-bolt +slack-bolt==1.15.2.dev0 pytest flake8==5.0.4 black==22.8.0 diff --git a/slack.json b/slack.json new file mode 100644 index 0000000..eeaa5ab --- /dev/null +++ b/slack.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "get-hooks": "bolt-get-hooks" + } +} \ No newline at end of file diff --git a/triggers/sample_trigger.json b/triggers/sample_trigger.json new file mode 100644 index 0000000..4a51c44 --- /dev/null +++ b/triggers/sample_trigger.json @@ -0,0 +1,14 @@ +{ + "type": "shortcut", + "name": "Sample trigger", + "description": "A sample trigger", + "workflow": "#/workflows/sample_workflow", + "inputs": { + "interactivity": { + "value": "{{data.interactivity}}" + }, + "channel": { + "value": "{{data.channel_id}}" + } + } +} \ No newline at end of file