-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Shunrao slack webhooks example #98
base: master
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Overview | ||
|
||
This project shows how Asana webhooks can be used to send a celebratory message to Slack whenever a milestone is completed. | ||
It also contains examples of how to manage webhooks through the Python Asana client. | ||
|
||
## Webhooks server | ||
Start the server before creating any webhooks | ||
``` | ||
ASANA_ACCESS_TOKEN=<your Asana PAT> SLACK_TOKEN=<your Slack API token> ./server.py | ||
``` | ||
|
||
## Manage Webhooks | ||
List existing webhooks: | ||
``` | ||
ASANA_ACCESS_TOKEN=<your Asana PAT> ./manage_webhooks.py list | ||
``` | ||
|
||
Create a new webhook: | ||
``` | ||
ASANA_ACCESS_TOKEN=<your Asana PAT> ./manage_webhooks.py create --resource <resource id> --target <target url> | ||
``` | ||
|
||
Delete a webhook by ID: | ||
``` | ||
ASANA_ACCESS_TOKEN=<your Asana PAT> ./manage_webhooks.py delete --id <webhook id> | ||
``` | ||
|
||
Delete ALL your webhooks: | ||
``` | ||
ASANA_ACCESS_TOKEN=<your Asana PAT> ./manage_webhooks.py delete --all | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
An example script showing ways to manage your webhooks using the | ||
python-asana client library, see README.md or run ./manage_webhooks.py -h | ||
for usage examples | ||
""" | ||
|
||
import argparse | ||
import os | ||
|
||
import asana | ||
|
||
WORKSPACE_ID = "15793206719" | ||
|
||
client = asana.Client.access_token(os.environ.get("ASANA_ACCESS_TOKEN")) | ||
WORKSPACE_NAME = client.workspaces.find_by_id(WORKSPACE_ID).get("name") | ||
|
||
|
||
def list_webhooks(): | ||
print( | ||
"Displaying webhooks you own associated with workspace: {}".format( | ||
WORKSPACE_NAME | ||
) | ||
) | ||
webhooks = client.webhooks.get_all({"workspace": WORKSPACE_ID}) | ||
for w in webhooks: | ||
print(w) | ||
|
||
|
||
def delete_webhook(gid): | ||
print("Deleting webhook: {}".format(gid)) | ||
client.webhooks.delete_by_id(gid) | ||
|
||
|
||
def delete_all_webhooks(): | ||
print( | ||
"Deleting all webhooks you own associated with workspace: {}".format( | ||
WORKSPACE_NAME | ||
) | ||
) | ||
webhooks = client.webhooks.get_all({"workspace": WORKSPACE_ID}) | ||
for w in webhooks: | ||
client.webhooks.delete_by_id(w.get("gid")) | ||
|
||
|
||
def create_webhook(resource, target): | ||
print( | ||
"Creating webhook on {}, make sure that the server at {} is ready to accept requests!".format( | ||
resource, target | ||
) | ||
) | ||
client.webhooks.create({"resource": resource, "target": target}) | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser() | ||
subparsers = parser.add_subparsers( | ||
help="What you want to do with Asana webhooks", dest="command", required=True | ||
) | ||
|
||
list_parser = subparsers.add_parser("list", help="List existing webhooks") | ||
|
||
create_parser = subparsers.add_parser("create", help="Create a new webhook") | ||
create_parser.add_argument( | ||
"--resource", | ||
type=str, | ||
help="A resource ID to subscribe to. The resource can be a task or project", | ||
required=True, | ||
) | ||
create_parser.add_argument( | ||
"--target", | ||
type=str, | ||
help="The webhook URL to receive the HTTP POST", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we describe an example of this if they run as per the instructions, just to make it clear that they're specifying a callback address they're setting up with the server file? I believe that'd be something like |
||
required=True, | ||
) | ||
|
||
delete_parser = subparsers.add_parser( | ||
"delete", help="Delete one or all existing webhooks" | ||
) | ||
delete_group = delete_parser.add_mutually_exclusive_group(required=True) | ||
delete_group.add_argument( | ||
"--id", type=str, help="The ID of the webhook you want to delete" | ||
) | ||
delete_group.add_argument("--all", action="store_true") | ||
|
||
args = parser.parse_args() | ||
|
||
if args.command == "list": | ||
list_webhooks() | ||
elif args.command == "create": | ||
create_webhook(args.resource, args.target) | ||
elif args.command == "delete": | ||
if args.all: | ||
delete_all_webhooks() | ||
else: | ||
delete_webhook(args.id) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
flask | ||
asana | ||
slackclient |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
#!/usr/bin/env python3 | ||
|
||
""" | ||
This script runs a Flask server that handles webhook requests from Asana, | ||
and sends a Slack notification whenever the server receives and event indicating | ||
that a milestone was completed. | ||
|
||
Instructions: | ||
1. Set the ASANA_ACCESS_TOKEN environment variable to your Personal Access Token | ||
2. Set the SLACK_TOKEN environment variable to your Slack API token | ||
3. Create a webhook using for your project using manage_webhooks.py | ||
""" | ||
|
||
import hmac | ||
import os | ||
import sys | ||
from hashlib import sha256 | ||
|
||
import asana | ||
from flask import Flask, Response, abort, request | ||
import slack | ||
|
||
SECRET_FILE = "/tmp/asana_webhook_secret" | ||
|
||
# We need an Asana client to fetch the full names of tasks and users | ||
# since webhook events are "skinny" | ||
client = asana.Client.access_token(os.environ.get("ASANA_ACCESS_TOKEN")) | ||
slack_client = slack.WebClient(token=os.environ.get("SLACK_TOKEN")) | ||
|
||
app = Flask(__name__) | ||
|
||
|
||
def load_webhook_secret(): | ||
with open(SECRET_FILE, "rb+") as f: | ||
return f.read().strip() | ||
|
||
|
||
def set_webhook_secret(value): | ||
with open(SECRET_FILE, "w+") as f: | ||
f.write(value) | ||
|
||
|
||
def validate_request(req): | ||
# Takes a flask request and computes a SHA256 HMAC using the webhook | ||
# secret we received from Asana and compares it with the signature provided | ||
# in the request as described in https://developers.asana.com/docs/#asana-webhooks. | ||
# Returns whether the two signatures match. | ||
|
||
request_signature = request.headers.get("X-Hook-Signature") | ||
computed_signature = hmac.new( | ||
load_webhook_secret(), msg=req.data, digestmod=sha256 | ||
).hexdigest() | ||
|
||
return hmac.compare_digest(computed_signature, request_signature) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👏 since this is optional to get a webhook payload I applaud you including it (I have a feeling a lot of apps don't even check...) |
||
|
||
|
||
def notify_slack(user, milestone): | ||
# Send a message to Slack for a milestone completion | ||
message = "{} just completed the milestone {}!".format( | ||
user.get("name"), milestone.get("name") | ||
) | ||
print('Sending message to Slack: "{}"'.format(message)) | ||
try: | ||
slack_client.chat_postMessage(channel="#asana-notifications", text=message) | ||
except slack.errors.SlackApiError: | ||
print("Error sending message to Slack.") | ||
|
||
|
||
@app.route("/slack_webhook", methods=["POST"]) | ||
def slack_webhook(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think it might be clearer to name this something a bit more clear that this is the callback url for Asana and not for, say, one from Slack. How do you feel about |
||
|
||
request_secret = request.headers.get("X-Hook-Secret") | ||
if request_secret: | ||
# On webhook creation, the server will be sent a request for which we respond with 200 OK | ||
# and a matching X-Hook-Secret header to confirm that we are accepting webhook requests | ||
|
||
# Note: resetting the local webhook secret whenever we get a new request to change | ||
set_webhook_secret(request_secret) | ||
|
||
resp = Response() | ||
resp.headers["X-Hook-Secret"] = load_webhook_secret() | ||
return resp | ||
elif not validate_request(request): | ||
# Return 403 Forbidden if the request signature doesn't match | ||
abort(403) | ||
|
||
events = request.json.get("events") | ||
for event in events: | ||
# Check if the event is the completion of a milestone | ||
if ( | ||
event["resource"].get("resource_subtype") == "marked_complete" | ||
and event["parent"].get("resource_subtype") == "milestone" | ||
): | ||
# The event only provides an id, so we need to get the full task | ||
# and user objects to use in the notification | ||
gid = event["parent"].get("gid") | ||
user_id = event["user"].get("gid") | ||
user = client.users.find_by_id(user_id) | ||
milestone = client.tasks.find_by_id(gid) | ||
notify_slack(user, milestone) | ||
|
||
return Response() | ||
|
||
|
||
if __name__ == "__main__": | ||
app.run(host="0.0.0.0", port=3000, debug=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth pointing out that the computer that this is running on needs to be DNS-addressable or have a public IP address that Asana can get to. This is often a point of confusion / friction for developers.