├── .flake8 ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── black.yml │ ├── flake8.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── app_oauth.py ├── listeners ├── __init__.py ├── actions │ ├── __init__.py │ └── sample_action.py ├── commands │ ├── __init__.py │ └── sample_command.py ├── events │ ├── __init__.py │ └── app_home_opened.py ├── messages │ ├── __init__.py │ └── sample_message.py ├── shortcuts │ ├── __init__.py │ └── sample_shortcut.py └── views │ ├── __init__.py │ └── sample_view.py ├── manifest.json ├── pyproject.toml ├── requirements.txt └── tests ├── __init__.py └── listeners ├── __init__.py ├── actions ├── __init__.py └── test_sample_action.py ├── commands ├── __init__.py └── test_sample_commands.py ├── events ├── __init__.py └── test_app_home_opened.py ├── messages ├── __init__.py └── test_sample_message.py ├── shortcuts ├── __init__.py └── test_sample_shortcut.py └── views ├── __init__.py └── test_sample_view.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 125 3 | exclude = .gitignore,venv 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners are the default owners for everything in 2 | # this repository. The owners listed below will be requested for 3 | # review when a pull request is opened. 4 | # To add code owner(s), uncomment the line below and 5 | # replace the @global-owner users with their GitHub username(s). 6 | # * @global-owner1 @global-owner2 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - "pip" 9 | - "dependencies" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Formatting validation using black 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | strategy: 13 | matrix: 14 | python-version: ["3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install -U pip 25 | pip install -r requirements.txt 26 | - name: Format with black 27 | run: | 28 | black . 29 | if git status --porcelain | grep .; then git --no-pager diff; exit 1; fi 30 | -------------------------------------------------------------------------------- /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: Linting validation using flake8 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | strategy: 13 | matrix: 14 | python-version: ["3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install -U pip 25 | pip install -r requirements.txt 26 | - name: Lint with flake8 27 | run: | 28 | flake8 *.py && flake8 listeners/ 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | strategy: 13 | matrix: 14 | python-version: ["3.11", "3.12", "3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install -U pip 25 | pip install -r requirements.txt 26 | - name: Run all tests 27 | run: | 28 | pytest . -v 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | docs/_sources/ 5 | docs/.doctrees 6 | .eggs/ 7 | *.egg-info/ 8 | *.egg 9 | *.py[cod] 10 | __pycache__/ 11 | *.so 12 | *~ 13 | 14 | # virtualenv 15 | env*/ 16 | venv/ 17 | .venv* 18 | .env* 19 | 20 | # codecov / coverage 21 | .coverage 22 | cov_* 23 | coverage.xml 24 | 25 | # due to using tox and pytest 26 | .tox 27 | .cache 28 | .pytest_cache/ 29 | .python-version 30 | pip 31 | .mypy_cache/ 32 | 33 | # misc 34 | tmp.txt 35 | .DS_Store 36 | logs/ 37 | *.db 38 | .pytype/ 39 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Slack Technologies, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bolt for Python Template App 2 | 3 | This is a generic Bolt for Python template app used to build out Slack apps. 4 | 5 | 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). 6 | ## Installation 7 | 8 | #### Create a Slack App 9 | 1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest" 10 | 2. Choose the workspace you want to install the application to 11 | 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* 12 | 4. Review the configuration and click *Create* 13 | 5. Click *Install to Workspace* and *Allow* on the screen that follows. You'll then be redirected to the App Configuration dashboard. 14 | 15 | #### Environment Variables 16 | Before you can run the app, you'll need to store some environment variables. 17 | 18 | 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`. 19 | 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`. 20 | 21 | ```zsh 22 | # Replace with your app token and bot token 23 | export SLACK_BOT_TOKEN= 24 | export SLACK_APP_TOKEN= 25 | ``` 26 | 27 | ### Setup Your Local Project 28 | ```zsh 29 | # Clone this project onto your machine 30 | git clone https://github.com/slack-samples/bolt-python-starter-template.git 31 | 32 | # Change into this project directory 33 | cd bolt-python-starter-template 34 | 35 | # Setup your python virtual environment 36 | python3 -m venv .venv 37 | source .venv/bin/activate 38 | 39 | # Install the dependencies 40 | pip install -r requirements.txt 41 | 42 | # Start your local server 43 | python3 app.py 44 | ``` 45 | 46 | #### Linting 47 | ```zsh 48 | # Run flake8 from root directory for linting 49 | flake8 *.py && flake8 listeners/ 50 | 51 | # Run black from root directory for code formatting 52 | black . 53 | ``` 54 | 55 | #### Testing 56 | ```zsh 57 | # Run pytest from root directory for unit testing 58 | pytest . 59 | ``` 60 | 61 | ## Project Structure 62 | 63 | ### `manifest.json` 64 | 65 | `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. 66 | 67 | ### `app.py` 68 | 69 | `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. 70 | 71 | ### `/listeners` 72 | 73 | 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. 74 | 75 | ## App Distribution / OAuth 76 | 77 | 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. 78 | 79 | 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. 80 | 81 | Start `ngrok` to access the app on an external network and create a redirect URL for OAuth. 82 | 83 | ``` 84 | ngrok http 3000 85 | ``` 86 | 87 | This output should include a forwarding address for `http` and `https` (we'll use `https`). It should look something like the following: 88 | 89 | ``` 90 | Forwarding https://3cb89939.ngrok.io -> http://localhost:3000 91 | ``` 92 | 93 | 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: 94 | 95 | ``` 96 | https://3cb89939.ngrok.io/slack/oauth_redirect 97 | ``` 98 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from slack_bolt import App 5 | from slack_bolt.adapter.socket_mode import SocketModeHandler 6 | 7 | from listeners import register_listeners 8 | 9 | logging.basicConfig(level=logging.DEBUG) 10 | 11 | # Initialization 12 | app = App(token=os.environ.get("SLACK_BOT_TOKEN")) 13 | 14 | # Register Listeners 15 | register_listeners(app) 16 | 17 | # Start Bolt app 18 | if __name__ == "__main__": 19 | SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start() 20 | -------------------------------------------------------------------------------- /app_oauth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from slack_bolt import App, BoltResponse 4 | from slack_bolt.oauth.callback_options import CallbackOptions, SuccessArgs, FailureArgs 5 | from slack_bolt.oauth.oauth_settings import OAuthSettings 6 | 7 | from slack_sdk.oauth.installation_store import FileInstallationStore 8 | from slack_sdk.oauth.state_store import FileOAuthStateStore 9 | 10 | from listeners import register_listeners 11 | 12 | logging.basicConfig(level=logging.DEBUG) 13 | 14 | 15 | # Callback to run on successful installation 16 | def success(args: SuccessArgs) -> BoltResponse: 17 | # Call default handler to return an HTTP response 18 | return args.default.success(args) 19 | # return BoltResponse(status=200, body="Installation successful!") 20 | 21 | 22 | # Callback to run on failed installation 23 | def failure(args: FailureArgs) -> BoltResponse: 24 | return args.default.failure(args) 25 | # return BoltResponse(status=args.suggested_status_code, body=args.reason) 26 | 27 | 28 | # Initialization 29 | app = App( 30 | signing_secret=os.environ.get("SLACK_SIGNING_SECRET"), 31 | installation_store=FileInstallationStore(), 32 | oauth_settings=OAuthSettings( 33 | client_id=os.environ.get("SLACK_CLIENT_ID"), 34 | client_secret=os.environ.get("SLACK_CLIENT_SECRET"), 35 | scopes=["channels:history", "chat:write", "commands"], 36 | user_scopes=[], 37 | redirect_uri=None, 38 | install_path="/slack/install", 39 | redirect_uri_path="/slack/oauth_redirect", 40 | state_store=FileOAuthStateStore(expiration_seconds=600), 41 | callback_options=CallbackOptions(success=success, failure=failure), 42 | ), 43 | ) 44 | 45 | # Register Listeners 46 | register_listeners(app) 47 | 48 | # Start Bolt app 49 | if __name__ == "__main__": 50 | app.start(3000) 51 | -------------------------------------------------------------------------------- /listeners/__init__.py: -------------------------------------------------------------------------------- 1 | from listeners import actions 2 | from listeners import commands 3 | from listeners import events 4 | from listeners import messages 5 | from listeners import shortcuts 6 | from listeners import views 7 | 8 | 9 | def register_listeners(app): 10 | actions.register(app) 11 | commands.register(app) 12 | events.register(app) 13 | messages.register(app) 14 | shortcuts.register(app) 15 | views.register(app) 16 | -------------------------------------------------------------------------------- /listeners/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | from .sample_action import sample_action_callback 3 | 4 | 5 | def register(app: App): 6 | app.action("sample_action_id")(sample_action_callback) 7 | -------------------------------------------------------------------------------- /listeners/actions/sample_action.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import Ack 4 | from slack_sdk import WebClient 5 | 6 | 7 | def sample_action_callback(ack: Ack, client: WebClient, body: dict, logger: Logger): 8 | try: 9 | ack() 10 | client.views_update( 11 | view_id=body["view"]["id"], 12 | hash=body["view"]["hash"], 13 | view={ 14 | "type": "modal", 15 | "callback_id": "sample_view_id", 16 | "title": { 17 | "type": "plain_text", 18 | "text": "Update modal title", 19 | }, 20 | "blocks": [ 21 | { 22 | "type": "section", 23 | "text": { 24 | "type": "mrkdwn", 25 | "text": "Nice! You updated the modal! 🎉", 26 | }, 27 | }, 28 | { 29 | "type": "image", 30 | "image_url": "https://media.giphy.com/media/SVZGEcYt7brkFUyU90/giphy.gif", 31 | "alt_text": "Yay! The modal was updated", 32 | }, 33 | { 34 | "type": "input", 35 | "block_id": "input_block_id", 36 | "label": { 37 | "type": "plain_text", 38 | "text": "What are your hopes and dreams?", 39 | }, 40 | "element": { 41 | "type": "plain_text_input", 42 | "action_id": "sample_input_id", 43 | "multiline": True, 44 | }, 45 | }, 46 | { 47 | "block_id": "select_channel_block_id", 48 | "type": "input", 49 | "label": { 50 | "type": "plain_text", 51 | "text": "Select a channel to message the result to", 52 | }, 53 | "element": { 54 | "type": "conversations_select", 55 | "action_id": "sample_dropdown_id", 56 | "response_url_enabled": True, 57 | }, 58 | }, 59 | ], 60 | "submit": {"type": "plain_text", "text": "Submit"}, 61 | }, 62 | ) 63 | except Exception as e: 64 | logger.error(e) 65 | -------------------------------------------------------------------------------- /listeners/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | from .sample_command import sample_command_callback 3 | 4 | 5 | def register(app: App): 6 | app.command("/sample-command")(sample_command_callback) 7 | -------------------------------------------------------------------------------- /listeners/commands/sample_command.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import Ack, Respond 2 | from logging import Logger 3 | 4 | 5 | def sample_command_callback(command, ack: Ack, respond: Respond, logger: Logger): 6 | try: 7 | ack() 8 | respond(f"Responding to the sample command! Your command was: {command['text']}") 9 | except Exception as e: 10 | logger.error(e) 11 | -------------------------------------------------------------------------------- /listeners/events/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | from .app_home_opened import app_home_opened_callback 3 | 4 | 5 | def register(app: App): 6 | app.event("app_home_opened")(app_home_opened_callback) 7 | -------------------------------------------------------------------------------- /listeners/events/app_home_opened.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_sdk import WebClient 4 | 5 | 6 | def app_home_opened_callback(client: WebClient, event: dict, logger: Logger): 7 | # ignore the app_home_opened event for anything but the Home tab 8 | if event["tab"] != "home": 9 | return 10 | try: 11 | client.views_publish( 12 | user_id=event["user"], 13 | view={ 14 | "type": "home", 15 | "blocks": [ 16 | { 17 | "type": "section", 18 | "text": { 19 | "type": "mrkdwn", 20 | "text": "*Welcome home, <@" + event["user"] + "> :house:*", 21 | }, 22 | }, 23 | { 24 | "type": "section", 25 | "text": { 26 | "type": "mrkdwn", 27 | "text": "Learn how home tabs can be more useful and " 28 | + "interactive .", 29 | }, 30 | }, 31 | ], 32 | }, 33 | ) 34 | except Exception as e: 35 | logger.error(f"Error publishing home tab: {e}") 36 | -------------------------------------------------------------------------------- /listeners/messages/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from slack_bolt import App 4 | from .sample_message import sample_message_callback 5 | 6 | 7 | # To receive messages from a channel or dm your app must be a member! 8 | def register(app: App): 9 | app.message(re.compile("(hi|hello|hey)"))(sample_message_callback) 10 | -------------------------------------------------------------------------------- /listeners/messages/sample_message.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import BoltContext, Say 4 | 5 | 6 | def sample_message_callback(context: BoltContext, say: Say, logger: Logger): 7 | try: 8 | greeting = context["matches"][0] 9 | say(f"{greeting}, how are you?") 10 | except Exception as e: 11 | logger.error(e) 12 | -------------------------------------------------------------------------------- /listeners/shortcuts/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | from .sample_shortcut import sample_shortcut_callback 3 | 4 | 5 | def register(app: App): 6 | app.shortcut("sample_shortcut_id")(sample_shortcut_callback) 7 | -------------------------------------------------------------------------------- /listeners/shortcuts/sample_shortcut.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import Ack 4 | from slack_sdk import WebClient 5 | 6 | 7 | def sample_shortcut_callback(body: dict, ack: Ack, client: WebClient, logger: Logger): 8 | try: 9 | ack() 10 | client.views_open( 11 | trigger_id=body["trigger_id"], 12 | view={ 13 | "type": "modal", 14 | "callback_id": "sample_view_id", 15 | "title": {"type": "plain_text", "text": "Sample modal title"}, 16 | "blocks": [ 17 | { 18 | "type": "section", 19 | "text": { 20 | "type": "mrkdwn", 21 | "text": "Click the button to update the modal", 22 | }, 23 | "accessory": { 24 | "type": "button", 25 | "text": {"type": "plain_text", "text": "Update modal"}, 26 | "action_id": "sample_action_id", 27 | }, 28 | }, 29 | { 30 | "type": "input", 31 | "block_id": "input_block_id", 32 | "label": { 33 | "type": "plain_text", 34 | "text": "What are your hopes and dreams?", 35 | }, 36 | "element": { 37 | "type": "plain_text_input", 38 | "action_id": "sample_input_id", 39 | "multiline": True, 40 | }, 41 | }, 42 | { 43 | "block_id": "select_channel_block_id", 44 | "type": "input", 45 | "label": { 46 | "type": "plain_text", 47 | "text": "Select a channel to message the result to", 48 | }, 49 | "element": { 50 | "type": "conversations_select", 51 | "action_id": "sample_dropdown_id", 52 | "response_url_enabled": True, 53 | }, 54 | }, 55 | ], 56 | "submit": {"type": "plain_text", "text": "Submit"}, 57 | }, 58 | ) 59 | except Exception as e: 60 | logger.error(e) 61 | -------------------------------------------------------------------------------- /listeners/views/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | from .sample_view import sample_view_callback 3 | 4 | 5 | def register(app: App): 6 | app.view("sample_view_id")(sample_view_callback) 7 | -------------------------------------------------------------------------------- /listeners/views/sample_view.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import Ack 4 | from slack_sdk import WebClient 5 | 6 | 7 | def sample_view_callback(view, ack: Ack, body: dict, client: WebClient, logger: Logger): 8 | try: 9 | ack() 10 | sample_user_value = body["user"]["id"] 11 | provided_values = view["state"]["values"] 12 | logger.info(f"Provided values {provided_values}") 13 | sample_input_value = provided_values["input_block_id"]["sample_input_id"]["value"] 14 | sample_convo_value = provided_values["select_channel_block_id"]["sample_dropdown_id"]["selected_conversation"] 15 | 16 | client.chat_postMessage( 17 | channel=sample_convo_value, 18 | text=f"<@{sample_user_value}> submitted the following :sparkles: " 19 | + f"hopes and dreams :sparkles:: \n\n {sample_input_value}", 20 | ) 21 | except Exception as e: 22 | logger.error(e) 23 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_metadata": { 3 | "major_version": 1, 4 | "minor_version": 1 5 | }, 6 | "display_information": { 7 | "name": "Bolt Template App" 8 | }, 9 | "features": { 10 | "app_home": { 11 | "home_tab_enabled": true, 12 | "messages_tab_enabled": false, 13 | "messages_tab_read_only_enabled": true 14 | }, 15 | "bot_user": { 16 | "display_name": "Bolt Template App", 17 | "always_online": false 18 | }, 19 | "shortcuts": [ 20 | { 21 | "name": "Run sample shortcut", 22 | "type": "global", 23 | "callback_id": "sample_shortcut_id", 24 | "description": "Runs a sample shortcut" 25 | } 26 | ], 27 | "slash_commands": [ 28 | { 29 | "command": "/sample-command", 30 | "description": "Runs a sample command", 31 | "should_escape": false 32 | } 33 | ] 34 | }, 35 | "oauth_config": { 36 | "scopes": { 37 | "bot": [ 38 | "channels:history", 39 | "chat:write", 40 | "commands" 41 | ] 42 | } 43 | }, 44 | "settings": { 45 | "event_subscriptions": { 46 | "bot_events": [ 47 | "app_home_opened", 48 | "message.channels" 49 | ] 50 | }, 51 | "interactivity": { 52 | "is_enabled": true 53 | }, 54 | "org_deploy_enabled": false, 55 | "socket_mode_enabled": true, 56 | "token_rotation_enabled": false 57 | } 58 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 125 3 | 4 | [tool.pytest.ini_options] 5 | testpaths = ["tests"] 6 | log_file = "logs/pytest.log" 7 | log_file_level = "DEBUG" 8 | log_format = "%(asctime)s %(levelname)s %(message)s" 9 | log_date_format = "%Y-%m-%d %H:%M:%S" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | slack-bolt==1.23.0 2 | pytest==8.3.5 3 | flake8==7.2.0 4 | black==25.1.0 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022, Slack Technologies, LLC. All rights reserved. 2 | -------------------------------------------------------------------------------- /tests/listeners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/__init__.py -------------------------------------------------------------------------------- /tests/listeners/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/actions/__init__.py -------------------------------------------------------------------------------- /tests/listeners/actions/test_sample_action.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_bolt import Ack 5 | from slack_sdk import WebClient 6 | 7 | from listeners.actions.sample_action import sample_action_callback 8 | 9 | test_logger = logging.getLogger(__name__) 10 | 11 | 12 | class TestSampleAction: 13 | def setup_method(self): 14 | self.fake_ack = Mock(Ack) 15 | self.fake_body = {"view": {"id": "test_id", "hash": "156772938.1827394"}} 16 | 17 | self.fake_client = Mock(WebClient) 18 | self.fake_client.views_update = Mock(WebClient.views_update) 19 | 20 | def test_sample_action_callback(self): 21 | sample_action_callback( 22 | ack=self.fake_ack, 23 | body=self.fake_body, 24 | client=self.fake_client, 25 | logger=test_logger, 26 | ) 27 | 28 | self.fake_ack.assert_called_once() 29 | 30 | self.fake_client.views_update.assert_called_once() 31 | kwargs = self.fake_client.views_update.call_args.kwargs 32 | assert kwargs["view_id"] == self.fake_body["view"]["id"] 33 | assert kwargs["hash"] == self.fake_body["view"]["hash"] 34 | assert kwargs["view"] is not None 35 | 36 | def test_ack_exception(self, caplog): 37 | self.fake_ack.side_effect = Exception("test exception") 38 | sample_action_callback( 39 | ack=self.fake_ack, 40 | body=self.fake_body, 41 | client=self.fake_client, 42 | logger=test_logger, 43 | ) 44 | 45 | self.fake_ack.assert_called_once() 46 | assert str(self.fake_ack.side_effect) in caplog.text 47 | -------------------------------------------------------------------------------- /tests/listeners/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/commands/__init__.py -------------------------------------------------------------------------------- /tests/listeners/commands/test_sample_commands.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_bolt import Ack, Respond 5 | 6 | from listeners.commands.sample_command import sample_command_callback 7 | 8 | 9 | test_logger = logging.getLogger(__name__) 10 | 11 | 12 | class TestSampleCommands: 13 | def setup_method(self): 14 | self.fake_ack = Mock(Ack) 15 | self.fake_respond = Mock(Respond) 16 | self.fake_command = {"text": "test command"} 17 | 18 | def test_sample_command_callback(self): 19 | sample_command_callback( 20 | self.fake_command, 21 | ack=self.fake_ack, 22 | respond=self.fake_respond, 23 | logger=test_logger, 24 | ) 25 | 26 | self.fake_ack.assert_called_once() 27 | 28 | self.fake_respond.assert_called_once() 29 | args = self.fake_respond.call_args.args 30 | assert self.fake_command["text"] in args[0] 31 | 32 | def test_ack_exception(self, caplog): 33 | self.fake_ack.side_effect = Exception("test exception") 34 | sample_command_callback( 35 | self.fake_command, 36 | ack=self.fake_ack, 37 | respond=self.fake_respond, 38 | logger=test_logger, 39 | ) 40 | 41 | self.fake_ack.assert_called_once() 42 | assert str(self.fake_ack.side_effect) in caplog.text 43 | -------------------------------------------------------------------------------- /tests/listeners/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/events/__init__.py -------------------------------------------------------------------------------- /tests/listeners/events/test_app_home_opened.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_sdk import WebClient 5 | 6 | from listeners.events.app_home_opened import app_home_opened_callback 7 | 8 | test_logger = logging.getLogger(__name__) 9 | 10 | 11 | class TestAppHomeOpened: 12 | def setup_method(self): 13 | self.fake_event = {"tab": "home", "user": "U123"} 14 | 15 | self.fake_client = Mock(WebClient) 16 | self.fake_client.views_publish = Mock(WebClient.views_publish) 17 | 18 | def test_app_home_opened_callback(self): 19 | app_home_opened_callback(client=self.fake_client, event=self.fake_event, logger=test_logger) 20 | 21 | self.fake_client.views_publish.assert_called_once() 22 | kwargs = self.fake_client.views_publish.call_args.kwargs 23 | assert kwargs["user_id"] == self.fake_event["user"] 24 | assert kwargs["view"] is not None 25 | 26 | def test_event_tab_not_home(self): 27 | self.fake_event["tab"] = "about" 28 | app_home_opened_callback(client=self.fake_client, event=self.fake_event, logger=test_logger) 29 | 30 | self.fake_client.views_publish.assert_not_called() 31 | 32 | def test_views_publish_exception(self, caplog): 33 | self.fake_client.views_publish.side_effect = Exception("test exception") 34 | app_home_opened_callback(client=self.fake_client, event=self.fake_event, logger=test_logger) 35 | 36 | self.fake_client.views_publish.assert_called_once() 37 | assert str(self.fake_client.views_publish.side_effect) in caplog.text 38 | -------------------------------------------------------------------------------- /tests/listeners/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/messages/__init__.py -------------------------------------------------------------------------------- /tests/listeners/messages/test_sample_message.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_bolt import BoltContext, Say 5 | 6 | from listeners.messages.sample_message import sample_message_callback 7 | 8 | 9 | test_logger = logging.getLogger(__name__) 10 | 11 | 12 | class TestSampleMessage: 13 | def setup_method(self): 14 | self.fake_say = Mock(Say) 15 | self.fake_context = BoltContext(matches=["hello"]) 16 | 17 | def test_sample_message_callback(self): 18 | sample_message_callback( 19 | context=self.fake_context, 20 | say=self.fake_say, 21 | logger=test_logger, 22 | ) 23 | 24 | self.fake_say.assert_called_once() 25 | args = self.fake_say.call_args.args 26 | assert self.fake_context.matches[0] in args[0] 27 | 28 | def test_say_exception(self, caplog): 29 | self.fake_say.side_effect = Exception("test exception") 30 | sample_message_callback( 31 | context=self.fake_context, 32 | say=self.fake_say, 33 | logger=test_logger, 34 | ) 35 | 36 | self.fake_say.assert_called_once() 37 | assert str(self.fake_say.side_effect) in caplog.text 38 | -------------------------------------------------------------------------------- /tests/listeners/shortcuts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/shortcuts/__init__.py -------------------------------------------------------------------------------- /tests/listeners/shortcuts/test_sample_shortcut.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_bolt import Ack 5 | from slack_sdk import WebClient 6 | 7 | from listeners.shortcuts.sample_shortcut import sample_shortcut_callback 8 | 9 | 10 | test_logger = logging.getLogger(__name__) 11 | 12 | 13 | class TestSampleShortcut: 14 | def setup_method(self): 15 | self.fake_ack = Mock(Ack) 16 | self.fake_body = {"trigger_id": "t1234"} 17 | self.fake_client = Mock(WebClient) 18 | self.fake_client.views_open = Mock(WebClient.views_open) 19 | 20 | def test_sample_shortcut_callback(self): 21 | sample_shortcut_callback(body=self.fake_body, ack=self.fake_ack, client=self.fake_client, logger=test_logger) 22 | 23 | self.fake_ack.assert_called_once() 24 | 25 | self.fake_client.views_open.assert_called_once() 26 | kwargs = self.fake_client.views_open.call_args.kwargs 27 | assert kwargs["trigger_id"] == self.fake_body["trigger_id"] 28 | assert kwargs["view"] is not None 29 | 30 | def test_ack_exception(self, caplog): 31 | self.fake_ack.side_effect = Exception("test exception") 32 | sample_shortcut_callback(body=self.fake_body, ack=self.fake_ack, client=self.fake_client, logger=test_logger) 33 | 34 | self.fake_client.views_open.assert_not_called() 35 | self.fake_ack.assert_called_once() 36 | assert str(self.fake_ack.side_effect) in caplog.text 37 | -------------------------------------------------------------------------------- /tests/listeners/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/bolt-python-starter-template/37ef80529971af7d717a18b2072112b26177bd27/tests/listeners/views/__init__.py -------------------------------------------------------------------------------- /tests/listeners/views/test_sample_view.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | from slack_bolt import Ack 5 | from slack_sdk import WebClient 6 | 7 | from listeners.views.sample_view import sample_view_callback 8 | 9 | 10 | test_logger = logging.getLogger(__name__) 11 | 12 | 13 | class TestSampleView: 14 | def setup_method(self): 15 | self.fake_ack = Mock(Ack) 16 | self.fake_body = {"user": {"id": "U1234"}} 17 | self.fake_view = { 18 | "state": { 19 | "values": { 20 | "input_block_id": {"sample_input_id": {"value": "test value"}}, 21 | "select_channel_block_id": {"sample_dropdown_id": {"selected_conversation": "C1234"}}, 22 | } 23 | } 24 | } 25 | self.fake_client = Mock(WebClient) 26 | self.fake_client.chat_postMessage = Mock(WebClient.chat_postMessage) 27 | 28 | def test_sample_view_callback(self): 29 | sample_view_callback( 30 | self.fake_view, 31 | ack=self.fake_ack, 32 | body=self.fake_body, 33 | client=self.fake_client, 34 | logger=test_logger, 35 | ) 36 | 37 | self.fake_ack.assert_called_once() 38 | 39 | self.fake_client.chat_postMessage.assert_called_once() 40 | kwargs = self.fake_client.chat_postMessage.call_args.kwargs 41 | assert ( 42 | kwargs["channel"] 43 | == self.fake_view["state"]["values"]["select_channel_block_id"]["sample_dropdown_id"]["selected_conversation"] 44 | ) 45 | assert self.fake_view["state"]["values"]["input_block_id"]["sample_input_id"]["value"] in kwargs["text"] 46 | assert self.fake_body["user"]["id"] in kwargs["text"] 47 | 48 | def test_ack_exception(self, caplog): 49 | self.fake_ack.side_effect = Exception("test exception") 50 | sample_view_callback( 51 | self.fake_view, 52 | ack=self.fake_ack, 53 | body=self.fake_body, 54 | client=self.fake_client, 55 | logger=test_logger, 56 | ) 57 | 58 | self.fake_ack.assert_called_once() 59 | assert str(self.fake_ack.side_effect) in caplog.text 60 | --------------------------------------------------------------------------------