Trigger Example with Slack
Integrate Slack slash commands with Valohai
You can launch triggers directly from Slack using slash commands, providing responses to the conversation from within the executions running through Valohai.
In this guide, we will be getting an image from a Slack conversation and passing it on to an execution that modifies it, then posting the result back to the Slack conversation.
While we will use a basic image manipulation script as the middle step as a visually obvious modification, you can easily plug in any Valohai execution that takes an input image and produces an output image in its place by changing the input names in the pipeline layout.
Slack API setup
You will need the message verification secret and Slack API token first.
Create a new Slack App.
When you are logged in to Slack chat in the app or web, navigate to the drop-down menu by your workspace name near the top left corner of the window. Under "Tools and Settings" you should find "Manage Apps." Click it to navigate to the Slack management portal.
On the Slack management portal, click the "Build" link on the top navigation bar. This will navigate you to the Slack developer portal.
Click on "Create new app"
Select "From app manifest".
Select the workspace to add the app to
Enter the app manifest (example manifest below). You should change the app name to your liking, or you can do it later.
Review the app's summary and finish creating the app. It should have these permissions:
channels:history,channels:join,channels:read,chat:write,commands,files:read,files:writeInstall the app to your Slack workspace.
Example app manifest:
{
"display_information": {
"name": "My Slack Integration"
},
"features": {
"bot_user": {
"display_name": "My Slack Integration",
"always_online": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"channels:history",
"channels:join",
"channels:read",
"chat:write",
"commands",
"files:read",
"files:write"
]
}
},
"settings": {
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}The integration requires two authentication tokens from your Slack app to interact with Slack.
Signing Secret: From your app summary page (where you installed it), browse down to find the Signing Secret. Click "Show" and copy the token into your notes.
Bot User OAuth Token: From the app summary page, browse the sidebar to "OAuth and Permissions." Click it to navigate to the OAuth configuration page. Copy the Bot User OAuth Token into your notes.
Valohai project setup
Add the following steps and pipeline to your valohai.yaml configuration file, commit the changes, push them to Git and fetch the new commit to Valohai.
Create a file called slack_input.py.
import sys
import os
from urllib.parse import parse_qs
import valohai
import requests
valohai.prepare(
step="slack-slash",
default_inputs={
'slack-webhook-input': '',
}
)
with open(valohai.inputs('slack-webhook-input').path()) as f:
slack_info = {k: v[0] for k, v in parse_qs(f.read()).items()}
print("Slack called us with:")
for key, value in slack_info.items():
print(f"{key}: {value}")
print("Replying to the slash command...")
parameters = slack_info.get("text")
if parameters:
reply = f"Hello from the execution! You told me to: {parameters}"
else:
reply = "Hello from the execution! I didn't get any parameters."
requests.post(slack_info["response_url"], json={
"text": reply
})
image_paths = [
# You can download multiple images at once
valohai.outputs().path("image1.png"),
]
convo_api = "https://slack.com/api/conversations.history"
slack_token = os.getenv("SLACK_TOKEN")
if slack_token:
print("Got slack token")
else:
requests.post(slack_info["response_url"], json={
"replace_original": True,
"text": "We don't have a SLACK_TOKEN! Please configure it in project settings."
})
sys.exit(3)
def download_image(url, destination):
with requests.get(url, stream=True, headers={"Authorization": f"Bearer {slack_token}"}) as r:
r.raise_for_status()
with open(destination, 'wb') as img:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
img.write(chunk)
image_filenames = []
def previous_image():
"""Browses Slack API and downloads two last posted images in the channel"""
resp = requests.get(convo_api, headers={"Authorization": f"Bearer {slack_token}"}, params={
"channel": slack_info["channel_id"]
})
resp.raise_for_status()
resp_json = resp.json()
print("Response:", resp_json)
for msg in resp_json["messages"]:
if not image_paths:
return True
if "files" in msg:
for att in msg["files"]:
if att["mimetype"] not in ["image/png", "image/jpeg"]:
continue
if "url_private_download" in att:
print("Found an image in files!")
path = image_paths.pop()
download_image(att["url_private_download"], path)
image_filenames.append(att["name"])
return not image_paths
search = previous_image
print(f"Querying Slack for images... params: {parameters}")
success = search()
if not success:
requests.post(slack_info["response_url"], json={
"text": ":pensive: Sorry, I could not find enough images with your command."
})
sys.exit(2)
user_id = slack_info['user_id']
print("Found images! They have been saved as outputs.")
requests.post(slack_info["response_url"], json={
"response_type": "in_channel",
"text": f"<@{user_id}> :rocket: Found image(s): {' ,'.join(image_filenames)}! Launching execution..."
})Create a file called slack_output.py.
import os
from urllib.parse import parse_qs
import valohai
import requests
valohai.prepare(
step="slack-upload",
default_inputs={
'slack-webhook-input': '',
'message-attachment': '',
}
)
with open(valohai.inputs('slack-webhook-input').path()) as f:
slack_info = {k: v[0] for k, v in parse_qs(f.read()).items()}
def check_ok(response_data):
if not response_data['ok']:
print("Slack problem: ", response_data)
slack_token = os.getenv("SLACK_TOKEN")
upload_context_api = "https://slack.com/api/files.getUploadURLExternal"
upload_finalize_api = "https://slack.com/api/files.completeUploadExternal"
print("Posting file")
file_path = valohai.inputs("message-attachment").path()
user_id = slack_info['user_id']
file_size = os.stat(file_path).st_size
upload_context_resp = requests.post(
upload_context_api,
data={
'filename': os.path.basename(file_path),
'length': file_size,
},
headers={"Authorization": f"Bearer {slack_token}"}
)
upload_context = upload_context_resp.json()
check_ok(upload_context)
print(f"Starting upload")
file_resp = requests.post(
upload_context["upload_url"],
headers={"Authorization": f"Bearer {slack_token}"},
files={'file': open(file_path, "rb")},
)
if not file_resp.content.startswith(b"OK"):
print("Upload problem:", file_resp.status_code, file_resp.content)
print("Wrapping up...")
finalize_resp = requests.post(
upload_finalize_api,
json={
'files': [{
'id': upload_context['file_id'],
'title': os.path.basename(file_path),
}],
'initial_comment': f"Hey <@{user_id}>, it's done! :shark:",
'channel_id': slack_info['channel_id'],
},
headers={"Authorization": f"Bearer {slack_token}"},
)
check_ok(finalize_resp.json())
print("Done!")Create a file called deepfry.py.
import valohai
from PIL import Image, ImageFilter
tmpfile = "intermediate.jpg"
def process_image(source_path):
img = Image.open(source_path)
xsize, ysize = img.size
delta = 1
# Make the image smaller
if xsize > 1000:
new_x = 1000
new_y = int(ysize * new_x/xsize)
else:
new_x, new_y = xsize, ysize
if new_y > 1000:
new_new_y = 1000
new_x = int(new_x * new_new_y/new_y)
new_y = new_new_y
else:
new_y, new_x = new_y, new_x
img = img.resize((new_x, new_y))
# Apply some filtering
for iteration in range(5):
img.filter(ImageFilter.EDGE_ENHANCE_MORE)
# Improve the color balance
img = img.convert("RGB")
r, g, b = img.split()
r.point(lambda i: min(i*2, 255))
g.point(lambda i: i*0.3)
b.point(lambda i: i*0.9)
img = Image.merge("RGB", (r, g, b))
# Repeatedly adjust image quality
for iteration in range(50):
left = img.crop((0, 0, delta, new_y))
right = img.crop((delta, 0, new_x, new_y))
img.paste(left, (new_x - delta, 0, new_x, new_y))
img.paste(right, (0, 0, new_x - delta, new_y))
img.save(tmpfile, 'jpeg', quality=10)
img = Image.open(tmpfile)
return img
def main():
processed_image = process_image(valohai.inputs("source_image").path())
# Save final file as an output
processed_image.save(valohai.outputs().path("fried_image.png"))
if __name__ == "__main__":
main()Add these steps and pipeline definition to your valohai.yaml:
- step:
name: deepfry-image
image: python:3.11
command:
- pip install valohai-utils pillow
- python ./deepfry.py
inputs:
- name: source_image
- step:
name: slack-slash
image: python:3.11
command:
- pip install valohai-utils requests
- python ./slack_input.py {parameters}
inputs:
- name: slack-webhook-input
optional: true
- step:
name: slack-upload
image: python:3.11
command:
- pip install valohai-utils requests
- python ./slack_output.py {parameters}
inputs:
- name: slack-webhook-input
optional: true
- name: message-attachment
optional: true
- pipeline:
name: Business Deepfrier 2024
edges:
- configuration: {}
source: slack-slash.output.image1.png
target: deepfry.input.source_image
- configuration: {}
source: slack-slash.input.slack-webhook-input
target: slack-upload.input.slack-webhook-input
- configuration: {}
source: deepfry.output.fried_image.png
target: slack-upload.input.message-attachment
nodes:
- name: slack-slash
on-error: stop-all
step: slack-slash
type: execution
- name: deepfry
on-error: stop-all
step: deepfry-image
type: execution
- name: slack-upload
on-error: stop-all
step: slack-upload
type: executionWe can now define the webhook trigger. Go to Project settings -> open Triggers tab -> create New Trigger
For type, select Webhook
Add a Web Request Authentication Condition
For Auth Type, select "HMAC"
For Token Lookup Namespace, select "HTTP Header"
For Token Lookup Key, type
X-Slack-SignatureFor Secret Key, type the Slack Signing Secret from your notes
For Value Prefix, type
v0=For HMAC Basestring Format, enter
v0:{header:X-Slack-Request-Timestamp}:{body}For Algorithm, select "SHA256"
Add a Web Request Timestamp condition
For Token Lookup Namespace, select "HTTP Header"
For Token Lookup Key, type
X-Slack-Request-TimestampTolerance of 60s is ok.
Add a Rate Limit condition
A Period of 60 seconds with a quota of 2-3 is fine.
As the slash command can be triggered on Slack any time, a rate limit can help limit excess usage.
Exceeding the rate limit will cause the webhook endpoint to return a 400 status response, and too many of these during a short time will cause Slack to disable the command. You may need to enable it again in the Slack developer portal.
Add a Run Pipeline action
For Source Commit Reference, type the primary branch of your Valohai project in Git. This is usually either
mainormasterFor Pipeline name, type "Business Deepfrier 2024"
For Payload Input Name, type "slack-slash.slack-webhook-input"
Add a Web Text Response action
For Response Text, type "Deep frying..."
This text displayed by Slack as the immediate response to the Slack slash command before the pipeline boots up. Feel free to customize this to add your own touch
Save trigger
You are now on the Triggers list again. Select the ... menu of the trigger we just created, and select "Edit" to return to editing the trigger. Get the trigger URL and save it to your notes.
Navigate to your Valohai project's Environment Variables settings, and add the Bot User OAuth Token from your notes as SLACK_TOKEN. You should mark it as a secret.
Slack slash command
Return to the Slack app portal. Select your integration app and navigate to "Slash Commands" on the sidebar.
Click "Create new command."
For Command, type
/deepfryFor Request URL, enter the Valohai trigger URL from the previous step
The short description and usage hint are optional.
Save the slash command.
Invite your bot user to a Slack conversation (DM or channel) in your workspace: /invite @My Slack Integration
Last updated
Was this helpful?
