Skip to content
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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/example-webhooks-slack/README.md
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
```

Copy link
Contributor Author

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.

## 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
```
96 changes: 96 additions & 0 deletions examples/example-webhooks-slack/manage_webhooks.py
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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 http://[computer_address]:3000/receive_webhook (if we change the route below...)

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)
3 changes: 3 additions & 0 deletions examples/example-webhooks-slack/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask
asana
slackclient
106 changes: 106 additions & 0 deletions examples/example-webhooks-slack/server.py
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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():
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 recieve_webhook or receive_asana_webhook?


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)