.. _django-guide:

=============================================================================
                             Django Integration
=============================================================================

.. contents:: Table of Contents:
    :local:
    :depth: 1

.. _django-installation:

Installation
============

To use Thorn with your Django project you must

#. Install Thorn

   .. code-block:: console

        $ pip install thorn

#. Add ``thorn.django`` to ``INSTALLED_APPS``:

   .. code-block:: python

        INSTALLED_APPS = (
            # ...,
            'thorn.django',
        )

#. Migrate your database to add the subscriber model:

    .. code-block:: console

        $ python manage.py migrate

#. Webhook-ify your models by adding the ``webhook_model`` decorator.

    Read all about it in the :ref:`Events Guide <events-guide>`.

#. (Optional) Install views for managing subscriptions:

    Only :ref:`Django REST Framework <django-rest-framework>` is supported
    yet, please help us by contributing more view types.

#. Specify the recommended HMAC signing method in your ``settings.py``:

    .. code-block:: python

        THORN_HMAC_SIGNER = 'thorn.utils.hmac:sign'

.. _django-rest-framework:

Django Rest Framework Views
===========================

The library comes with a standard set of views you can add to your
Django Rest Framework API, that enables your users to subscribe and
unsubscribe from events.

The views all map to the :class:`~thorn.django.models.Subscriber` model.

To enable them add the following URL configuration to your
:file:`urls.py`:

.. code-block:: python

    url(r"^hooks/",
        include("thorn.django.rest_framework.urls", namespace="webhook"))

.. _django-rest-framework-operations:

Supported operations
--------------------

.. note::

    All of these curl examples omit the important detail
    that you need to be logged in as a user of your API.

.. django-rest-framework-subscribe:

Subscribing to events
~~~~~~~~~~~~~~~~~~~~~

Adding a new subscription is as simple as posting to the ``/hooks/`` endpoint,
including the mandatory event and url arguments:

.. code-block:: bash

    $ curl -X POST                                                      \
    > -H "Content-Type: application/json"                               \
    > -d '{"event": "article.*", "url": "https://e.com/h/article?u=1"}' \
    > http://example.com/hooks/

Returns the response:

.. code-block:: json

    {"id": "c91fe938-55fb-4190-a5ed-bd92f5ea8339",
     "url": "http:\/\/e.com\/h/article?u=1",
     "created_at": "2016-01-13T23:12:52.205785Z",
     "updated_at": "2016-01-13T23:12:52.205813Z",
     "user": 1,
     "hmac_secret": "C=JTX)v3~dQCl];[_h[{q{CScm]oglLoe&>ga:>R~jR$.x?t|kW!FH:s@|4bu:11",
     "hmac_digest": "sha256",
     "content_type": "application\/json",
     "subscription": "http://localhost/hooks/c91fe938-55fb-4190-a5ed-bd92f5ea8339",
     "event": "article.*"}

**Parameters**

- ``event`` (mandatory)

    The type of event you want to receive notifications about.  Events are
    composed of dot-separated words, so this argument can also be specified
    as a pattern matching words in the event name (e.g. ``article.*``,
    ``*.created``, or ``article.created``).

- ``url`` (mandatory)

    The URL destination where the event will be sent to, using
    a HTTP POST request.

- ``content_type`` (optional)

    The content type argument specifies the MIME-type of the format
    required by your endpoint.  The default is ``application/json``,
    but you can also specify ``application/x-www-form-urlencoded.``.

- ``hmac_digest`` (optional)

    Specify custom HMAC digest type, which must be one of: sha1, sha256, sha512.

    Default is sha256.

- ``hmac_secret`` (optional)

    Specify custom HMAC secret key.

    This key can be used to verify the sender of webhook events received.

    A random key of 64 characters in length will be generated by default,
    and can be found in the response.

The only important part of the response data at this stage is the ``id``,
which is the unique identifier for this subscription, and the ``subscription`` url
which you can use to unsubscribe later.

.. _django-rest-framework-list-subscriptions:

Listing subscriptions
~~~~~~~~~~~~~~~~~~~~~

Perform a *GET* request on the ``/hooks/`` endpoint to retrieve a list of
all the subscriptions owned by user:

.. code-block:: bash

    $ curl -X GET                                       \
    > -H "Content-Type: application/json"               \
    > http://example.com/hooks/

Returns the response:

.. code-block:: json

    [
        {"id": "c91fe938-55fb-4190-a5ed-bd92f5ea8339",
         "url": "http:\/\/e.com\/h/article?u=1",
         "created_at": "2016-01-15T23:12:52.205785Z",
         "updated_at": "2016-01-15T23:12:52.205813Z",
         "user": 1,
         "content_type": "application\/json",
         "event": "article.*"}
    ]

.. _django-rest-framework-unsubscribe:

Unsubscribing from events
~~~~~~~~~~~~~~~~~~~~~~~~~~

Perform a *DELETE* request on the ``/hooks/<UUID>`` endpoint to unsubscribe
from a subscription by unique identifier:

.. code-block:: bash

    $ curl -X DELETE                                    \
    > -H "Content-Type: application/json"               \
    > http://example.com/hooks/c91fe938-55fb-4190-a5ed-bd92f5ea8339/

.. _django-example-consumer:

Example consumer endpoint
=========================

This is an example Django webhook receiver view, using the json content type:

.. code-block:: python

    from __future__ import absolute_import, unicode_literals

    import hmac
    import base64
    import json
    import hashlib

    from itsdangerous import URLSafeSerializer

    from django.http import HttpResponse
    from django.views.decorators.http import require_POST
    from django.views.decorators.csrf import csrf_exempt

    ARTICLE_SECRET = 'C=JTX)v3~dQCl];[_h[{q{CScm]oglLoe&>ga:>R~jR$.x?t|kW!FH:s@|4bu:11'
    ARTICLE_DIGEST_TYPE = 'sha256'

    def verify_webhook(secret, hmac_header, digest_method, message):
        digestmod = getattr(hashlib, digest_method)
        signed = base64.b64encode(
            hmac.new(secret, message, digestmod).digest(),
        ).strip()
        return hmac.compare_digest(signed, hmac_header)

    @require_POST()
    @csrf_exempt()
    def handle_article_changed(request):
        digest = request.META.get('HTTP_HOOK_HMAC')
        digest_type = request.META.get('')
        body = request.body
        if verify_webhook(ARTICLE_SECRET, digest, ARTICLE_DIGEST_TYPE, body):
            payload = json.loads(body)
            print('Article changed: {0[ref]}'.format(payload)
            return HttpResponse(status=200)

Using the :func:`~django.views.decorators.csrf.csrf_exempt` is important here,
as by default Django will refuse POST requests that do not specify the CSRF
protection token.


Verify HMAC Ruby
================

This example is derived from Shopify's great examples found here:
https://help.shopify.com/api/tutorials/webhooks#verify-webhook

.. code-block:: ruby

    require 'rubygems'
    require 'base64'
    require 'openssl'
    require 'sinatra'

    ARTICLE_SECRET = 'C=JTX)v3~dQCl];[_h[{q{CScm]oglLoe&>ga:>R~jR$.x?t|kW!FH:s@|4bu:11'

    helpers do

        def verify_webhook(secret, data, hmac_header):
            digest = OpenSSL::Digest::Digest.new('sha256')
            calculated_hmac = Base64.encode64(OpenSSL:HMAC.digest(
                digest, secret, data)).strip
            return calculated_hmac == hmac_header
        end
    end

    post '/' do
        request.body.rewind
        data = request.body.read
        if verify_webhook(ARTICLE_SECRET, env["HTTP_HOOK_HMAC"])
            # deserialize data' using json and process webhook
        end
    end

Verify HMAC PHP
===============

This example is derived from Shopify's great examples found here:
https://help.shopify.com/api/tutorials/webhooks#verify-webhook

.. code-block:: php

    <?php

    define('ARTICLE_SECRET', 'C=JTX)v3~dQCl];[_h[{q{CScm]oglLoe&>ga:>R~jR$.x?t|kW!FH:s@|4bu:11')

    function verify_webhook($data, $hmac_header)
    {
        $calculated_hmac = base64_encode(hash_hmac('sha256', $data,
            ARTICLE_SECRET, true));
        return ($hmac_header == $calculated_hmac);
    }

    $hmac_header = $_SERVER['HTTP_HOOK_HMAC'];
    $data = file_get_contents('php://input');
    $verified = verify_webhook($data, $hmac_header);

    ?>

