Skip to content

Commit 6d6c64e

Browse files
Merge pull request #1 from slack-samples/init-improvements
feat: improve the sample to follow patterns from other templates
2 parents a39fd0a + b22dae7 commit 6d6c64e

20 files changed

Lines changed: 748 additions & 230 deletions

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
pip install -U pip
25-
pip install -r requirements.txt
25+
pip install ".[dev]"
2626
- name: Lint with ruff
2727
run: |
2828
ruff check

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
pip install -U pip
25-
pip install -r requirements.txt
25+
pip install ".[dev]"
2626
- name: Run all tests
2727
run: |
2828
pytest .

.slack/apps.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,130 @@
1-
# bolt-python-search-template
2-
A template for implementing enterprise search in Bolt for Python
1+
# Bolt for Python Search Template
2+
3+
> ⚠️ **Beta Notice**: This template demonstrates search functionality that is currently in beta and not yet widely available to all developers. The features shown here are being tested and may change before general availability.
4+
5+
This is a Slack app template for building search functionality using Bolt for Python. It demonstrates how to create custom functions for search and filtering capabilities.
6+
7+
It's recommended to use a [Slack sandbox](https://docs.slack.dev/tools/developer-sandboxes/) for development and testing. Other workspaces may not have access to all these features. To get started:
8+
9+
1. Join the [Slack Developer Program](https://api.slack.com/developer-program) if you haven't already
10+
2. [Provision a sandbox workspace](https://docs.slack.dev/tools/developer-sandboxes/#provision)
11+
12+
## Installation
13+
14+
### Using Slack CLI
15+
16+
Install the latest version of the Slack CLI for your operating system:
17+
18+
- [Slack CLI for macOS & Linux](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux/)
19+
- [Slack CLI for Windows](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-windows/)
20+
21+
You'll also need to log in if this is your first time using the Slack CLI.
22+
23+
```sh
24+
slack login
25+
```
26+
27+
#### Initializing the project
28+
29+
```sh
30+
slack create bolt-python-search --template slack-samples/bolt-python-search-template -branch init
31+
cd bolt-python-search
32+
33+
# Setup your python virtual environment
34+
python3 -m venv .venv
35+
source .venv/bin/activate
36+
37+
# Install the dependencies
38+
pip install .
39+
```
40+
41+
#### Creating the Slack app
42+
43+
```sh
44+
slack install
45+
```
46+
47+
#### Running the app
48+
49+
```sh
50+
slack run
51+
```
52+
53+
<details>
54+
<summary><h3>Using Terminal</h3></summary>
55+
56+
1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest"
57+
2. Choose the workspace you want to install the application to
58+
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_
59+
4. Review the configuration and click _Create_
60+
5. Click _Install to Workspace_ and _Allow_ on the screen that follows. You'll then be redirected to the App Configuration dashboard.
61+
62+
#### Environment Variables
63+
64+
Before you can run the app, you'll need to store some environment variables.
65+
66+
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`.
67+
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`.
68+
69+
```zsh
70+
# Replace with your app token and bot token
71+
export SLACK_BOT_TOKEN=<your-bot-token>
72+
export SLACK_APP_TOKEN=<your-app-token>
73+
```
74+
75+
### Setup Your Local Project
76+
77+
```sh
78+
# Clone this project onto your machine
79+
git clone https://github.com/slack-samples/bolt-python-search-template.git
80+
81+
# Change into this project directory
82+
cd bolt-python-search-template
83+
84+
# Setup your python virtual environment
85+
python3 -m venv .venv
86+
source .venv/bin/activate
87+
88+
# Install all the dependencies
89+
pip install -e ".[dev]"
90+
91+
# Start your local server
92+
python3 app.py
93+
```
94+
95+
</details>
96+
97+
## Linting
98+
99+
```sh
100+
# Run ruff from root directory for linting
101+
ruff check
102+
103+
# Run ruff from root directory for formatting
104+
ruff format && ruff check --fix
105+
```
106+
107+
## Testing
108+
109+
```sh
110+
# Run pytest from root directory for unit testing
111+
pytest .
112+
```
113+
114+
## Project Structure
115+
116+
### `manifest.json`
117+
118+
`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.
119+
120+
### `app.py`
121+
122+
`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.
123+
124+
### `/listeners`
125+
126+
Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming [Events](https://docs.slack.dev/reference/events) requests, `/listeners/functions` handles [custom steps](https://docs.slack.dev/tools/bolt-js/concepts/custom-steps) and so on.
127+
128+
### `/test`
129+
130+
The `/test` directory contains the test suite for this project. It mirrors the structure of the source code, with test files corresponding to their implementation counterparts. For example, tests for files in `/listeners/functions` are located in `/test/listeners/functions`.

app.py

Lines changed: 5 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,16 @@
1-
import json
21
import logging
32
import os
4-
from enum import Enum
5-
from typing import TypedDict, List, Optional, NotRequired
63

7-
from slack_bolt import Ack, App, Complete, Fail
4+
from slack_bolt import App
85
from slack_bolt.adapter.socket_mode import SocketModeHandler
9-
from slack_sdk import WebClient
106

11-
logging.basicConfig(level=logging.INFO)
12-
13-
#----------------------
14-
# MODELS
15-
#----------------------
16-
17-
class FilterType(Enum):
18-
MULTI_SELECT = "multi_select"
19-
TOGGLE = "toggle"
20-
21-
class EntityReference(TypedDict):
22-
id: str
23-
type: Optional[str]
24-
25-
class SearchResult(TypedDict):
26-
title: str
27-
description: str
28-
link: str
29-
date_updated: str
30-
external_ref: EntityReference
31-
content: NotRequired[str]
32-
33-
34-
class FilterOptions(TypedDict):
35-
name: str
36-
value: str
37-
38-
39-
class SearchFilter(TypedDict):
40-
name: str
41-
display_name: str
42-
filter_type: FilterType
43-
options: Optional[List[FilterOptions]]
44-
45-
46-
client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"), base_url="https://slack.com/api/")
47-
app = App(client=client)
48-
49-
50-
@app.function("search", auto_acknowledge=False)
51-
def handle_search_step_event(
52-
ack: Ack, inputs: dict, body: dict, fail: Fail, complete: Complete, logger: logging.Logger
53-
):
54-
55-
try:
56-
query = inputs.get("query")
57-
languages_filter = inputs.get("filters", {}).get("languages", [])
58-
type_filter = inputs.get("filters", {}).get("type", [])
59-
60-
filters_payload = {}
61-
if languages_filter:
62-
filters_payload["languages"] = languages_filter
63-
if type_filter:
64-
if len(type_filter) == 1:
65-
filters_payload["type"] = type_filter[0]
66-
67-
params = {
68-
"filters": json.dumps(filters_payload)
69-
}
70-
if query:
71-
params["query"] = query
72-
73-
logger.info(f"Calling developer.sampleData.get with filters: {filters_payload}")
74-
75-
response = client.api_call(
76-
api_method="developer.sampleData.get",
77-
params=params
78-
)
7+
from listeners import register_listeners
798

80-
if not response.get("ok"):
81-
logger.error(f"API error: {response}")
82-
fail(error="Failed to fetch sample data")
83-
return
84-
85-
samples = response.get("samples", [])
86-
87-
results: List[SearchResult] = [
88-
{
89-
"title": sample["title"],
90-
"description": sample["description"],
91-
"link": sample["link"],
92-
"date_updated": sample["date_updated"],
93-
"external_ref": sample["external_ref"],
94-
**({"content": sample["content"]} if "content" in sample else {}),
95-
}
96-
for sample in samples
97-
]
98-
99-
complete(outputs={"search_result": results})
100-
101-
except Exception as e:
102-
logger.error(f"Error in search step: {str(e)}")
103-
fail(error=str(e))
104-
finally:
105-
ack()
106-
107-
108-
@app.function("filters", auto_acknowledge=False)
109-
def handle_filters_step_event(
110-
ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger
111-
):
112-
try:
113-
filters: List[SearchFilter] = [
114-
{
115-
"name": "languages",
116-
"display_name": "Languages",
117-
"type": FilterType.MULTI_SELECT.value,
118-
"options": [
119-
{"name": "Python", "value": "python"},
120-
{"name": "Java", "value": "java"},
121-
{"name": "JavaScript", "value": "javascript"},
122-
{"name": "TypeScript", "value": "typescript"},
123-
],
124-
},
125-
{
126-
"name": "type",
127-
"display_name": "Type",
128-
"type": FilterType.MULTI_SELECT.value,
129-
"options": [
130-
{"name": "Template", "value": "template"},
131-
{"name": "Sample", "value": "sample"},
132-
],
133-
},
134-
]
135-
136-
complete(outputs={"filters": filters})
137-
finally:
138-
ack()
139-
140-
@app.event("entity_details_requested")
141-
def handle_flexpane_event(event, body, client, logger):
142-
sample_id = event["external_ref"]["id"]
143-
144-
response = client.api_call(api_method="developer.sampleData.get")
145-
146-
if not response.get("ok"):
147-
logger.error(f"Failed to fetch samples: {response}")
148-
return
149-
150-
samples = response.get("samples", [])
151-
152-
sample = next((s for s in samples if s["external_ref"]["id"] == sample_id), None)
153-
154-
if not sample:
155-
logger.warning(f"No matching sample found for id={sample_id}")
156-
return
157-
158-
159-
custom_fields = [
160-
{
161-
"key": "description",
162-
"label": "Description of sample",
163-
"type": "string",
164-
"value": sample["description"],
165-
},
166-
{
167-
"key": "date_updated",
168-
"label": "Last updated",
169-
"type": "string",
170-
"value": sample["date_updated"],
171-
},
172-
]
173-
174-
if "content" in sample:
175-
custom_fields.append({
176-
"key": "content",
177-
"label": "Details of sample",
178-
"type": "string",
179-
"value": sample["content"],
180-
})
9+
logging.basicConfig(level=logging.INFO)
18110

182-
payload = {
183-
"trigger_id": event["trigger_id"],
184-
"metadata": {
185-
"entity_type": "slack#/entities/item",
186-
"url": event["link"]["url"],
187-
"external_ref": {"id": sample_id},
188-
"entity_payload": {
189-
"attributes": {
190-
"title": {
191-
"text": sample["title"],
192-
"edit": {
193-
"enabled": False,
194-
"text": {
195-
"max_length": 50
196-
}
197-
}
198-
},
199-
},
200-
"custom_fields": custom_fields,
201-
},
202-
},
203-
}
11+
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
20412

205-
try:
206-
client.api_call(
207-
api_method="entity.presentDetails",
208-
json=payload,
209-
)
210-
except Exception as e:
211-
logger.error(f"Error calling entity.presentDetails: {str(e)}")
13+
register_listeners(app)
21214

21315
if __name__ == "__main__":
21416
SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start()

listeners/__init__.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
from slack_bolt import App
1+
from listeners import events, functions
22

3-
from .functions import handle_search_event, handle_filters_event
43

5-
6-
def register_listeners(app: App):
7-
app.function("search", auto_acknowledge=False, ack_timeout=10)(handle_search_event)
8-
app.function("filters", auto_acknowledge=False)(handle_filters_event)
4+
def register_listeners(app):
5+
functions.register(app)
6+
events.register(app)

listeners/events/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from slack_bolt import App
2+
3+
from .entity_details_requested import entity_details_requested_callback
4+
5+
6+
def register(app: App):
7+
app.event("entity_details_requested")(entity_details_requested_callback)

0 commit comments

Comments
 (0)