2018-05-31 01:56:18 +02:00
# Incoming webhook walkthrough
2017-02-06 22:07:45 +01:00
2018-05-31 01:56:18 +02:00
Below, we explain each part of a simple incoming webhook integration,
called **Hello World** . This integration sends a "hello" message to the `test`
2018-01-26 13:37:54 +01:00
stream and includes a link to the Wikipedia article of the day, which
it formats from json data it receives in the http request.
2017-02-06 22:07:45 +01:00
Use this walkthrough to learn how to write your first webhook
integration.
## Step 0: Create fixtures
2018-05-31 01:56:18 +02:00
The first step in creating an incoming webhook is to examine the data that the
2017-02-06 22:07:45 +01:00
service you want to integrate will be sending to Zulip.
2021-04-09 03:50:04 +02:00
* Use [Zulip's JSON integration ](/integrations/doc/json ),
< https: / / webhook . site / > , or a similar tool to capture webhook
payload(s) from the service you are integrating. Examining this data
allows you to do two things:
2017-02-06 22:07:45 +01:00
1. Determine how you will need to structure your webhook code, including what
2018-10-17 03:38:18 +02:00
message types your integration should support and how.
2017-02-06 22:07:45 +01:00
2. Create fixtures for your webhook tests.
A test fixture is a small file containing test data, one for each test.
Fixtures enable the testing of webhook integration code without the need to
actually contact the service being integrated.
2018-05-31 01:56:18 +02:00
Because `Hello World` is a very simple integration that does one
thing, it requires only one fixture,
`zerver/webhooks/helloworld/fixtures/hello.json` :
2017-02-06 22:07:45 +01:00
2021-05-26 18:48:00 +02:00
```json
2017-02-06 22:07:45 +01:00
{
"featured_title":"Marilyn Monroe",
"featured_url":"https://en.wikipedia.org/wiki/Marilyn_Monroe",
}
```
2018-05-31 01:56:18 +02:00
When writing your own incoming webhook integration, you'll want to write a test function
for each distinct message condition your integration supports. You'll also need a
2017-02-09 23:10:08 +01:00
corresponding fixture for each of these tests. Depending on the type of data
the 3rd party service sends, your fixture may contain JSON, URL encoded text, or
2019-04-14 15:28:19 +02:00
some other kind of data. See [Step 5: Create automated tests ](#step-5-create-automated-tests ) or
2017-11-16 17:36:52 +01:00
[Testing ](https://zulip.readthedocs.io/en/latest/testing/testing.html ) for further details.
2017-02-06 22:07:45 +01:00
2019-06-05 15:12:34 +02:00
### HTTP Headers
Some third-party webhook APIs, such as GitHub's, don't encode all the
information about an event in the JSON request body. Instead, they
put key details like the event type in a separate HTTP header
(generally this is clear in their API documentation). In order to
test Zulip's handling of that integration, you will need to record
which HTTP headers are used with each fixture you capture.
2019-06-22 06:57:40 +02:00
Since this is integration-dependent, Zulip offers a simple API for
doing this, which is probably best explained by looking at the example
for GitHub: `zerver/webhooks/github/view.py` ; basically, as part of
writing your integration, you'll write a special function in your
view.py file that maps the filename of the fixture to the set of HTTP
headers to use. This function must be named "fixture_to_headers". Most
integrations will use the same strategy as the GitHub integration:
encoding the third party variable header data (usually just an event
type) in the fixture filename, in such a case, you won't need to
explicitly write the logic for such a special function again,
instead you can just use the same helper method that the GitHub
integration uses.
2019-06-05 15:12:34 +02:00
2017-02-06 22:07:45 +01:00
## Step 1: Initialize your webhook python package
In the `zerver/webhooks/` directory, create new subdirectory that will
contain all of corresponding code. In our example it will be
`helloworld` . The new directory will be a python package, so you have
2018-01-26 13:37:54 +01:00
to create an empty `__init__.py` file in that directory via e.g.
`touch zerver/webhooks/helloworld/__init__.py` .
2017-02-06 22:07:45 +01:00
## Step 2: Create main webhook code
2018-05-31 01:56:18 +02:00
The majority of the code for your new integration will be in a single
2017-02-06 22:07:45 +01:00
python file, `zerver/webhooks/mywebhook/view.py` .
The Hello World integration is in `zerver/webhooks/helloworld/view.py` :
2021-05-26 18:48:00 +02:00
```python
2021-04-30 00:15:33 +02:00
from typing import Any, Dict, Sequence
2017-02-06 22:07:45 +01:00
from django.http import HttpRequest, HttpResponse
2018-03-13 23:36:11 +01:00
2020-08-20 00:32:15 +02:00
from zerver.decorator import webhook_view
2018-03-13 23:36:11 +01:00
from zerver.lib.request import REQ, has_request_variables
2021-04-30 00:15:33 +02:00
from zerver.lib.response import json_success
from zerver.lib.webhooks.common import check_send_webhook_message
2018-03-13 23:36:11 +01:00
from zerver.models import UserProfile
2017-02-06 22:07:45 +01:00
2021-04-30 00:15:33 +02:00
@webhook_view ("HelloWorld")
2017-02-06 22:07:45 +01:00
@has_request_variables
2018-03-13 23:36:11 +01:00
def api_helloworld_webhook(
2021-04-30 00:15:33 +02:00
request: HttpRequest,
user_profile: UserProfile,
payload: Dict[str, Sequence[Dict[str, Any]]] = REQ(argument_type="body"),
2018-03-13 23:36:11 +01:00
) -> HttpResponse:
2017-08-24 17:31:04 +02:00
# construct the body of the message
2021-04-30 00:15:33 +02:00
body = "Hello! I am happy to be here! :smile:"
2017-02-06 22:07:45 +01:00
2017-08-24 17:31:04 +02:00
# try to add the Wikipedia article of the day
2021-04-30 00:15:33 +02:00
body_template = (
"\nThe Wikipedia featured article for today is ** [{featured_title} ]({featured_url} )**"
)
2017-08-24 17:31:04 +02:00
body += body_template.format(**payload)
2017-02-06 22:07:45 +01:00
2018-03-13 23:36:11 +01:00
topic = "Hello World"
2017-08-24 17:31:04 +02:00
# send the message
2018-03-13 23:36:11 +01:00
check_send_webhook_message(request, user_profile, topic, body)
2017-02-06 22:07:45 +01:00
2022-02-04 16:15:07 +01:00
return json_success(request)
2017-02-06 22:07:45 +01:00
```
The above code imports the required functions and defines the main webhook
2020-08-20 00:32:15 +02:00
function `api_helloworld_webhook` , decorating it with `webhook_view` and
2017-02-09 23:10:08 +01:00
`has_request_variables` . The `has_request_variables` decorator allows you to
access request variables with `REQ()` . You can find more about `REQ` and request
2017-11-16 17:36:52 +01:00
variables in [Writing views](
https://zulip.readthedocs.io/en/latest/tutorials/writing-views.html#request-variables).
2017-02-06 22:07:45 +01:00
2018-05-31 01:56:18 +02:00
You must pass the name of your integration to the
2020-08-20 00:32:15 +02:00
`webhook_view` decorator; that name will be used to
2018-05-31 01:56:18 +02:00
describe your integration in Zulip's analytics (e.g. the `/stats`
page). Here we have used `HelloWorld` . To be consistent with other
integrations, use the name of the product you are integrating in camel
case, spelled as the product spells its own name (except always first
letter upper-case).
2017-02-06 22:07:45 +01:00
2020-08-20 00:32:15 +02:00
The `webhook_view` decorator indicates that the 3rd party service will
2017-02-09 23:10:08 +01:00
send the authorization as an API key in the query parameters. If your service uses
2020-10-23 02:43:28 +02:00
HTTP basic authentication, you would instead use the `authenticated_rest_api_view`
2017-02-09 23:10:08 +01:00
decorator.
2018-05-31 01:56:18 +02:00
You should name your webhook function as such
`api_webhookname_webhook` where `webhookname` is the name of your
integration and is always lower-case.
2017-02-06 22:07:45 +01:00
At minimum, the webhook function must accept `request` (Django
2021-11-05 20:26:37 +01:00
[HttpRequest ](https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.HttpRequest )
2017-05-02 01:00:50 +02:00
object), and `user_profile` (Zulip's user object). You may also want to
define additional parameters using the `REQ` object.
2017-02-06 22:07:45 +01:00
In the example above, we have defined `payload` which is populated
from the body of the http request, `stream` with a default of `test`
(available by default in the Zulip development environment), and
2017-02-09 23:10:08 +01:00
`topic` with a default of `Hello World` . If your webhook uses a custom stream,
it must exist before a message can be created in it. (See
2019-04-14 15:28:19 +02:00
[Step 4: Create automated tests ](#step-5-create-automated-tests ) for how to handle this in tests.)
2017-02-06 22:07:45 +01:00
The line that begins `# type` is a mypy type annotation. See [this
2018-12-17 06:13:21 +01:00
page](https://zulip.readthedocs.io/en/latest/testing/mypy.html) for details about
2017-11-16 17:36:52 +01:00
how to properly annotate your webhook functions.
2017-02-06 22:07:45 +01:00
In the body of the function we define the body of the message as `Hello! I am
happy to be here! :smile:`. The `:smile:` indicates an emoji. Then we append a
2017-08-24 17:31:04 +02:00
link to the Wikipedia article of the day as provided by the json payload.
* Sometimes, it might occur that a json payload does not contain all required keys your
integration checks for. In such a case, any `KeyError` thrown is handled by the server
backend and will create an appropriate response.
2017-02-06 22:07:45 +01:00
2018-03-13 23:36:11 +01:00
Then we send a message with `check_send_webhook_message` , which will
validate the message and do the following:
* Send a public (stream) message if the `stream` query parameter is
specified in the webhook URL.
* If the `stream` query parameter isn't specified, it will send a private
message to the owner of the webhook bot.
2017-02-06 22:07:45 +01:00
Finally, we return a 200 http status with a JSON format success message via
2022-02-04 16:15:07 +01:00
`json_success(request)` .
2017-02-06 22:07:45 +01:00
2020-10-23 02:43:28 +02:00
## Step 3: Create an API endpoint for the webhook
2017-02-06 22:07:45 +01:00
2018-05-31 01:56:18 +02:00
In order for an incoming webhook to be externally available, it must be mapped
2020-10-23 02:43:28 +02:00
to a URL. This is done in `zerver/lib/integrations.py` .
2017-02-06 22:07:45 +01:00
Look for the lines beginning with:
2021-05-26 18:48:00 +02:00
```python
2017-02-06 22:07:45 +01:00
WEBHOOK_INTEGRATIONS = [
```
And you'll find the entry for Hello World:
2021-05-26 18:48:00 +02:00
```python
2017-08-21 22:07:49 +02:00
WebhookIntegration('helloworld', ['misc'], display_name='Hello World'),
2017-02-06 22:07:45 +01:00
```
2020-10-23 02:43:28 +02:00
This tells the Zulip API to call the `api_helloworld_webhook` function in
2017-02-06 22:07:45 +01:00
`zerver/webhooks/helloworld/view.py` when it receives a request at
`/api/v1/external/helloworld` .
This line also tells Zulip to generate an entry for Hello World on the Zulip
integrations page using `static/images/integrations/logos/helloworld.png` as its
2017-08-21 22:07:49 +02:00
icon. The second positional argument defines a list of categories for the
integration.
2017-02-06 22:07:45 +01:00
At this point, if you're following along and/or writing your own Hello World
2019-04-14 15:28:19 +02:00
webhook, you have written enough code to test your integration. There are three
tools which you can use to test your webhook - 2 command line tools and a GUI.
2017-02-06 22:07:45 +01:00
2019-08-18 12:56:21 +02:00
### Webhooks requiring custom configuration
In rare cases, it's necessary for an incoming webhook to require
additional user configuration beyond what is specified in the post
URL. The typical use case for this is APIs like the Stripe API that
require clients to do a callback to get details beyond an opaque
object ID that one would want to include in a Zulip notification.
These configuration options are declared as follows:
2021-05-26 18:48:00 +02:00
```python
2019-08-18 12:56:21 +02:00
WebhookIntegration('helloworld', ['misc'], display_name='Hello World',
2021-05-10 07:02:14 +02:00
config_options=[('HelloWorld API key', 'hw_api_key', check_string)])
2019-08-18 12:56:21 +02:00
```
`config_options` is a list describing the parameters the user should
configure:
1. A user-facing string describing the field to display to users.
2. The field name you'll use to access this from your `view.py` function.
3. A Validator, used to verify the input is valid.
Common validators are available in `zerver/lib/validators.py` .
2019-04-14 15:28:19 +02:00
## Step 4: Manually testing the webhook
2017-02-06 22:07:45 +01:00
2021-07-01 11:45:22 +02:00
For either one of the command line tools, first, you'll need to get an
API key from the **Bots** section of your Zulip user's **Personal
settings**. To test the webhook, you'll need to [create a
bot](https://zulip.com/help/add-a-bot-or-integration) with the
**Incoming webhook** type. Replace `<api_key>` with your bot's API key
in the examples presented below! This is how Zulip knows that the
request was made by an authorized user.
2017-02-06 22:07:45 +01:00
2019-04-14 15:28:19 +02:00
### Curl
2017-02-06 22:07:45 +01:00
2019-04-14 15:28:19 +02:00
Using curl:
2021-05-26 18:48:00 +02:00
```bash
2019-04-14 15:28:19 +02:00
curl -X POST -H "Content-Type: application/json" -d '{ "featured_title":"Marilyn Monroe", "featured_url":"https://en.wikipedia.org/wiki/Marilyn_Monroe" }' http://localhost:9991/api/v1/external/helloworld\?api_key\=< api_key >
2017-02-06 22:07:45 +01:00
```
2019-04-14 15:28:19 +02:00
After running the above command, you should see something similar to:
2017-02-06 22:07:45 +01:00
2021-05-26 18:48:00 +02:00
```json
2019-04-14 15:28:19 +02:00
{"msg":"","result":"success"}
2017-02-06 22:07:45 +01:00
```
2021-05-10 07:02:14 +02:00
### Management command: send_webhook_fixture_message
2017-02-06 22:07:45 +01:00
2019-04-14 15:28:19 +02:00
Using `manage.py` from within the Zulip development environment:
2017-02-06 22:07:45 +01:00
2021-05-26 18:48:00 +02:00
```console
2022-04-19 03:18:48 +02:00
(zulip-py3-venv) vagrant@vagrant:/srv/zulip$
2019-04-14 15:28:19 +02:00
./manage.py send_webhook_fixture_message \
--fixture=zerver/webhooks/helloworld/fixtures/hello.json \
'--url=http://localhost:9991/api/v1/external/helloworld?api_key=< api_key > '
2017-02-06 22:07:45 +01:00
```
2019-04-14 15:28:19 +02:00
After running the above command, you should see something similar to:
2017-02-06 22:07:45 +01:00
2019-04-14 15:28:19 +02:00
```
2016-07-07 15:06:59,187 INFO 127.0.0.1 POST 200 143ms (mem: 6ms/13) (md: 43ms/1) (db: 20ms/9q) (+start: 147ms) /api/v1/external/helloworld (helloworld-bot@zulip.com via ZulipHelloWorldWebhook)
```
2017-02-06 22:07:45 +01:00
2019-03-09 11:01:20 +01:00
Some webhooks require custom HTTP headers, which can be passed using
`./manage.py send_webhook_fixture_message --custom-headers` . For
example:
--custom-headers='{"X-Custom-Header": "value"}'
The format is a JSON dictionary, so make sure that the header names do
not contain any spaces in them and that you use the precise quoting
approach shown above.
2019-04-14 15:28:19 +02:00
### Integrations Dev Panel
This is the GUI tool.
1. Run `./tools/run-dev.py` then go to http://localhost:9991/devtools/integrations/.
2020-10-23 02:43:28 +02:00
2. Set the following mandatory fields:
**Bot** - Any incoming webhook bot.
**Integration** - One of the integrations.
2019-05-16 09:23:15 +02:00
**Fixture** - Though not mandatory, it's recommended that you select one and then tweak it if necessary.
2019-04-14 15:28:19 +02:00
The remaining fields are optional, and the URL will automatically be generated.
3. Click **Send** !
2019-05-16 09:23:15 +02:00
By opening Zulip in one tab and then this tool in another, you can quickly tweak
2019-04-14 15:28:19 +02:00
your code and send sample messages for many different test fixtures.
2019-05-16 09:23:15 +02:00
Note: Custom HTTP Headers must be entered as a JSON dictionary, if you want to use any in the first place that is.
Feel free to use 4-spaces as tabs for indentation if you'd like!
2019-04-14 15:28:19 +02:00
Your sample notification may look like:
2019-06-19 10:09:53 +02:00
< img class = "screenshot" src = "/static/images/api/helloworld-webhook.png" alt = "screenshot" / >
2019-04-14 15:28:19 +02:00
## Step 5: Create automated tests
2017-02-06 22:07:45 +01:00
Every webhook integration should have a corresponding test file:
`zerver/webhooks/mywebhook/tests.py` .
2017-02-09 23:10:08 +01:00
The Hello World integration's tests are in `zerver/webhooks/helloworld/tests.py`
2017-02-06 22:07:45 +01:00
You should name the class `<WebhookName>HookTests` and have it inherit from
the base class `WebhookTestCase` . For our HelloWorld webhook, we name the test
class `HelloWorldHookTests` :
2021-05-26 18:48:00 +02:00
```python
2017-02-06 22:07:45 +01:00
class HelloWorldHookTests(WebhookTestCase):
STREAM_NAME = 'test'
URL_TEMPLATE = "/api/v1/external/helloworld?& api_key={api_key}"
2021-08-14 01:01:37 +02:00
WEBHOOK_DIR_NAME = 'helloworld'
2017-02-06 22:07:45 +01:00
# Note: Include a test function per each distinct message condition your integration supports
2017-11-05 03:44:12 +01:00
def test_hello_message(self) -> None:
2018-11-09 20:33:58 +01:00
expected_topic = "Hello World";
2017-11-04 05:16:55 +01:00
expected_message = "Hello! I am happy to be here! :smile: \nThe Wikipedia featured article for today is ** [Marilyn Monroe ](https://en.wikipedia.org/wiki/Marilyn_Monroe )**";
2017-02-06 22:07:45 +01:00
# use fixture named helloworld_hello
2020-08-23 15:49:24 +02:00
self.check_webhook('hello', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
2017-02-06 22:07:45 +01:00
```
2021-08-14 01:01:37 +02:00
In the above example, `STREAM_NAME` , `URL_TEMPLATE` , and `WEBHOOK_DIR_NAME` refer
2017-02-06 22:07:45 +01:00
to class attributes from the base class, `WebhookTestCase` . These are needed by
2020-08-23 15:49:24 +02:00
the helper function `check_webhook` to determine how to execute
2017-02-09 23:10:08 +01:00
your test. `STREAM_NAME` should be set to your default stream. If it doesn't exist,
2020-08-23 15:49:24 +02:00
`check_webhook` will create it while executing your test.
2017-02-09 23:10:08 +01:00
If your test expects a stream name from a test fixture, the value in the fixture
and the value you set for `STREAM_NAME` must match. The test helpers use `STREAM_NAME`
to create the destination stream, and then create the message to send using the
value from the fixture. If these don't match, the test will fail.
2018-05-31 01:56:18 +02:00
`URL_TEMPLATE` defines how the test runner will call your incoming webhook, in the same way
2017-02-09 23:10:08 +01:00
you would provide a webhook URL to the 3rd party service. `api_key={api_key}` says
that an API key is expected.
2017-02-06 22:07:45 +01:00
When writing tests for your webhook, you'll want to include one test function
(and corresponding fixture) per each distinct message condition that your
integration supports.
If, for example, we added support for sending a goodbye message to our `Hello
World` webhook, we would add another test function to `HelloWorldHookTests`
class called something like `test_goodbye_message` :
2021-05-26 18:48:00 +02:00
```python
2017-11-05 03:44:12 +01:00
def test_goodbye_message(self) -> None:
2018-11-09 20:33:58 +01:00
expected_topic = "Hello World";
2017-11-04 05:16:55 +01:00
expected_message = "Hello! I am happy to be here! :smile:\nThe Wikipedia featured article for today is ** [Goodbye ](https://en.wikipedia.org/wiki/Goodbye )**";
2017-02-06 22:07:45 +01:00
# use fixture named helloworld_goodbye
2020-08-23 15:49:24 +02:00
self.check_webhook('goodbye', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
2017-02-06 22:07:45 +01:00
```
2017-05-14 00:43:35 +02:00
As well as a new fixture `goodbye.json` in
2017-04-27 21:35:08 +02:00
`zerver/webhooks/helloworld/fixtures/` :
2017-02-06 22:07:45 +01:00
2021-05-26 18:48:00 +02:00
```json
2017-02-06 22:07:45 +01:00
{
"featured_title":"Goodbye",
"featured_url":"https://en.wikipedia.org/wiki/Goodbye",
}
```
2017-02-09 23:10:08 +01:00
Also consider if your integration should have negative tests, a test where the
2017-02-16 19:37:49 +01:00
data from the test fixture should result in an error. For details see
[Negative tests ](#negative-tests ), below.
2017-02-09 23:10:08 +01:00
2017-02-06 22:07:45 +01:00
Once you have written some tests, you can run just these new tests from within
the Zulip development environment with this command:
2021-05-26 18:48:00 +02:00
```console
2022-04-19 03:18:48 +02:00
(zulip-py3-venv) vagrant@vagrant:/srv/zulip$
2017-02-06 22:07:45 +01:00
./tools/test-backend zerver/webhooks/helloworld
```
(Note: You must run the tests from the top level of your development directory.
The standard location in a Vagrant environment is `/srv/zulip` . If you are not
using Vagrant, use the directory where you have your development environment.)
You will see some script output and if all the tests have passed, you will see:
2021-05-26 18:48:00 +02:00
```console
2017-02-06 22:07:45 +01:00
Running zerver.webhooks.helloworld.tests.HelloWorldHookTests.test_goodbye_message
Running zerver.webhooks.helloworld.tests.HelloWorldHookTests.test_hello_message
DONE!
```
2019-04-14 15:28:19 +02:00
## Step 6: Create documentation
2017-02-06 22:07:45 +01:00
2018-05-31 01:56:18 +02:00
Next, we add end-user documentation for our integration. You
2020-06-08 23:04:39 +02:00
can see the existing examples at < https: / / zulip . com / integrations >
2017-02-19 18:59:34 +01:00
or by accessing `/integrations` in your Zulip development environment.
2017-02-06 22:07:45 +01:00
There are two parts to the end-user documentation on this page.
The first is the lozenge in the grid of integrations, showing your
integration logo and name, which links to the full documentation.
This is generated automatically once you've registered the integration
2017-06-30 04:03:22 +02:00
in `WEBHOOK_INTEGRATIONS` in `zerver/lib/integrations.py` , and supports
2017-02-06 22:07:45 +01:00
some customization via options to the `WebhookIntegration` class.
Second, you need to write the actual documentation content in
2017-06-30 04:03:22 +02:00
`zerver/webhooks/mywebhook/doc.md` .
2017-02-06 22:07:45 +01:00
2021-05-26 18:48:00 +02:00
```md
2017-06-30 04:03:22 +02:00
Learn how Zulip integrations work with this simple Hello World example!
The Hello World webhook will use the `test` stream, which is
created by default in the Zulip dev environment. If you are running
Zulip in production, you should make sure that this stream exists.
2017-11-16 17:36:52 +01:00
Next, on your {{ settings_html|safe }}, create a Hello World bot.
2017-06-30 04:03:22 +02:00
Construct the URL for the Hello World bot using the API key and
stream name:
2017-10-30 22:04:15 +01:00
`{{ api_url }}/v1/external/helloworld?api_key=abcdefgh&stream=test`
2017-06-30 04:03:22 +02:00
To trigger a notification using this webhook, use
`send_webhook_fixture_message` from the Zulip command line:
2017-02-06 22:07:45 +01:00
2022-04-19 03:18:48 +02:00
(zulip-py3-venv) vagrant@vagrant:/srv/zulip$
2017-06-30 04:03:22 +02:00
./manage.py send_webhook_fixture_message \
2018-04-19 20:17:24 +02:00
--fixture=zerver/tests/fixtures/helloworld/hello.json \
2017-11-22 19:26:57 +01:00
'--url=http://localhost:9991/api/v1/external/helloworld?api_key=< api_key> '
2017-06-30 04:03:22 +02:00
Or, use curl:
curl -X POST -H "Content-Type: application/json" -d '{ "featured_title":"Marilyn Monroe", "featured_url":"https://en.wikipedia.org/wiki/Marilyn_Monroe" }' http://localhost:9991/api/v1/external/helloworld\?api_key\=< api_key>
{!congrats.md!}
2020-08-05 11:50:17 +02:00
![Hello World integration ](/static/images/integrations/helloworld/001.png )
2017-02-06 22:07:45 +01:00
```
2017-06-30 04:03:22 +02:00
`{!congrats.md!}` is an example of a Markdown macro. Zulip has a macro-based
Markdown/Jinja2 framework that includes macros for common instructions in
Zulip's webhooks/integrations documentation.
2017-10-27 00:08:59 +02:00
See
2018-10-10 00:21:18 +02:00
[our guide on documenting an integration][integration-docs-guide]
2017-10-27 00:08:59 +02:00
for further details, including how to easily create the message
2018-10-17 03:38:18 +02:00
screenshot. Mostly you should plan on templating off an existing guide, like
2021-09-01 00:15:31 +02:00
[this one ](https://raw.githubusercontent.com/zulip/zulip/main/zerver/webhooks/github/doc.md ).
2017-02-06 22:07:45 +01:00
2019-05-30 00:39:50 +02:00
[integration-docs-guide]: https://zulip.readthedocs.io/en/latest/documentation/integrations.html
2018-10-10 00:21:18 +02:00
2019-04-14 15:28:19 +02:00
## Step 7: Preparing a pull request to zulip/zulip
2017-02-06 22:07:45 +01:00
When you have finished your webhook integration and are ready for it to be
available in the Zulip product, follow these steps to prepare your pull
request:
1. Run tests including linters and ensure you have addressed any issues they
2017-11-16 17:36:52 +01:00
report. See [Testing ](https://zulip.readthedocs.io/en/latest/testing/testing.html )
and [Linters ](https://zulip.readthedocs.io/en/latest/testing/linters.html ) for details.
2. Read through [Code styles and conventions](
https://zulip.readthedocs.io/en/latest/contributing/code-style.html) and take a look
2017-02-06 22:07:45 +01:00
through your code to double-check that you've followed Zulip's guidelines.
2020-10-23 02:43:28 +02:00
3. Take a look at your Git history to ensure your commits have been clear and
2021-05-10 07:02:14 +02:00
logical (see [Version control](
2017-11-16 17:36:52 +01:00
https://zulip.readthedocs.io/en/latest/contributing/version-control.html) for tips). If not,
2018-05-31 01:56:18 +02:00
consider revising them with `git rebase --interactive` . For most incoming webhooks,
2017-02-06 22:07:45 +01:00
you'll want to squash your changes into a single commit and include a good,
clear commit message.
4. Push code to your fork.
5. Submit a pull request to zulip/zulip.
2017-02-09 23:10:08 +01:00
If you would like feedback on your integration as you go, feel free to post a
message on the [public Zulip instance ](https://chat.zulip.org/#narrow/stream/bots ).
2022-06-24 06:33:47 +02:00
You can also create a [draft pull request](
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests) while you
2017-11-16 17:36:52 +01:00
are still working on your integration. See the
2017-11-24 01:24:00 +01:00
[Git guide ](https://zulip.readthedocs.io/en/latest/git/pull-requests.html#create-a-pull-request )
2017-11-16 17:36:52 +01:00
for more on Zulip's pull request process.
2017-02-16 19:37:49 +01:00
## Advanced topics
More complex implementation or testing needs may require additional code, beyond
what the standard helper functions provide. This section discusses some of
these situations.
### Negative tests
A negative test is one that should result in an error, such as incorrect data.
The helper functions may interpret this as a test failure, when it should instead
be a successful test of an error condition. To correctly test these cases, you
must explicitly code your test's execution (using other helpers, as needed)
rather than call the usual helper function.
2018-05-31 01:56:18 +02:00
Here is an example from the WordPress integration:
2017-02-16 19:37:49 +01:00
2021-05-26 18:48:00 +02:00
```python
2017-11-05 03:44:12 +01:00
def test_unknown_action_no_data(self) -> None:
2020-08-23 15:49:24 +02:00
# Mimic check_webhook() to manually execute a negative test.
2020-08-23 19:09:27 +02:00
# Otherwise its call to send_webhook_payload() would assert on the non-success
2017-02-16 19:37:49 +01:00
# we are testing. The value of result is the error message the webhook should
# return if no params are sent. The fixture for this test is an empty file.
# subscribe to the target stream
2017-08-25 06:37:47 +02:00
self.subscribe(self.test_user, self.STREAM_NAME)
2017-02-16 19:37:49 +01:00
# post to the webhook url
post_params = {'stream_name': self.STREAM_NAME,
'content_type': 'application/x-www-form-urlencoded'}
result = self.client_post(self.url, 'unknown_action', **post_params)
# check that we got the expected error message
2021-05-10 07:02:14 +02:00
self.assert_json_error(result, "Unknown WordPress webhook action: WordPress action")
2017-02-16 19:37:49 +01:00
```
2020-08-23 15:49:24 +02:00
In a normal test, `check_webhook` would handle all the setup
2018-05-31 01:56:18 +02:00
and then check that the incoming webhook's response matches the expected result. If
2017-02-16 19:37:49 +01:00
the webhook returns an error, the test fails. Instead, explicitly do the
setup it would have done, and check the result yourself.
Here, `subscribe_to_stream` is a test helper that uses `TEST_USER_EMAIL` and
`STREAM_NAME` (attributes from the base class) to register the user to receive
messages in the given stream. If the stream doesn't exist, it creates it.
2018-05-31 01:56:18 +02:00
`client_post` , another helper, performs the HTTP POST that calls the incoming
webhook. As long as `self.url` is correct, you don't need to construct the webhook
2017-02-16 19:37:49 +01:00
URL yourself. (In most cases, it is.)
`assert_json_error` then checks if the result matches the expected error.
2020-08-23 15:49:24 +02:00
If you had used `check_webhook` , it would have called
2020-08-23 19:09:27 +02:00
`send_webhook_payload` , which checks the result with `assert_json_success` .
2017-02-16 19:37:49 +01:00
### Custom query parameters
Custom arguments passed in URL query parameters work as expected in the webhook
code, but require special handling in tests.
For example, here is the definition of a webhook function that gets both `stream`
and `topic` from the query parameters:
2021-05-26 18:48:00 +02:00
```python
2017-11-05 03:44:12 +01:00
def api_querytest_webhook(request: HttpRequest, user_profile: UserProfile,
payload: str=REQ(argument_type='body'),
stream: str=REQ(default='test'),
topic: str=REQ(default='Default Alert')):
2017-02-16 19:37:49 +01:00
```
In actual use, you might configure the 3rd party service to call your Zulip
integration with a URL like this:
```
http://myhost/api/v1/external/querytest?api_key=abcdefgh& stream=alerts& topic=queries
```
It provides values for `stream` and `topic` , and the webhook can get those
using `REQ` without any special handling. How does this work in a test?
2017-04-23 23:50:46 +02:00
The new attribute `TOPIC` exists only in our class so far. In order to
construct a URL with a query parameter for `topic` , you can pass the
attribute `TOPIC` as a keyword argument to `build_webhook_url` , like so:
2017-02-16 19:37:49 +01:00
2021-05-26 18:48:00 +02:00
```python
2017-02-16 19:37:49 +01:00
class QuerytestHookTests(WebhookTestCase):
STREAM_NAME = 'querytest'
2021-05-10 07:02:14 +02:00
TOPIC = "Default topic"
2017-04-23 23:50:46 +02:00
URL_TEMPLATE = "/api/v1/external/querytest?api_key={api_key}& stream={stream}"
2017-02-16 19:37:49 +01:00
FIXTURE_DIR_NAME = 'querytest'
2017-11-05 03:44:12 +01:00
def test_querytest_test_one(self) -> None:
2017-02-16 19:37:49 +01:00
# construct the URL used for this test
2021-05-10 07:02:14 +02:00
self.TOPIC = "Query test"
2017-04-23 23:50:46 +02:00
self.url = self.build_webhook_url(topic=self.TOPIC)
2017-02-16 19:37:49 +01:00
# define the expected message contents
2021-05-10 07:02:14 +02:00
expected_topic = "Query test"
2017-11-04 05:16:55 +01:00
expected_message = "This is a test of custom query parameters."
2017-02-16 19:37:49 +01:00
2020-08-23 15:49:24 +02:00
self.check_webhook('test_one', expected_topic, expected_message,
2017-02-16 19:37:49 +01:00
content_type="application/x-www-form-urlencoded")
```
2020-08-20 21:18:02 +02:00
You can also override `get_body` or `get_payload` if your test data
needs to be constructed in an unusual way.
For more, see the definition for the base class, `WebhookTestCase`
in `zerver/lib/test_classes.py` , or just grep for examples.
2018-04-24 20:22:38 +02:00
### Custom HTTP event-type headers
Some third-party services set a custom HTTP header to indicate the event type that
generates a particular payload. To extract such headers, we recommend using the
`validate_extract_webhook_http_header` function in `zerver/lib/webhooks/common.py` ,
like so:
2021-05-26 18:48:00 +02:00
```python
2018-04-24 20:22:38 +02:00
event = validate_extract_webhook_http_header(request, header, integration_name)
```
`request` is the `HttpRequest` object passed to your main webhook function. `header`
2022-05-12 06:54:12 +02:00
is the name of the custom header you'd like to extract, such as `X-Event-Key` , and
2018-04-24 20:22:38 +02:00
`integration_name` is the name of the third-party service in question, such as
`GitHub` .
Because such headers are how some integrations indicate the event types of their
payloads, the absence of such a header usually indicates a configuration
issue, where one either entered the URL for a different integration, or happens to
be running an older version of the integration that doesn't set that header.
If the requisite header is missing, this function sends a PM to the owner of the
webhook bot, notifying them of the missing header.
2018-05-22 16:45:21 +02:00
### Handling unexpected webhook event types
2020-08-20 00:50:06 +02:00
Many third-party services have dozens of different event types. In
some cases, we may choose to explicitly ignore specific events. In
other cases, there may be events that are new or events that we don't
know about. In such cases, we recommend raising
2022-11-17 09:30:48 +01:00
`UnsupportedWebhookEventTypeError` (found in `zerver/lib/exceptions.py` ),
2020-08-20 00:50:06 +02:00
with a string describing the unsupported event type, like so:
2018-05-22 16:45:21 +02:00
```
2022-11-17 09:30:48 +01:00
raise UnsupportedWebhookEventTypeError(event_type)
2018-05-22 16:45:21 +02:00
```