File

mod_rest/README.markdown @ 3820:d3757e089433

mod_rest: Add a JSON callback example
author Kim Alvefur <zash@zash.se>
date Wed, 01 Jan 2020 18:11:55 +0100
parent 3813:aa1ad69c7c10
child 3826:21ffca4d3aae
line wrap: on
line source

---
labels:
- 'Stage-Alpha'
summary: RESTful XMPP API
---

# Introduction

This is yet another RESTful API for sending and receiving stanzas via
Prosody. It can be used to build bots and components implemented as HTTP
services.

# Usage

## Enabling

``` {.lua}
Component "rest.example.net" "rest"
rest_credentials = "Bearer dmVyeSBzZWNyZXQgdG9rZW4K"
```

## Sending stanzas

The API endpoint becomes available at the path `/rest`, so the full URL
will be something like `https://your-prosody.example:5281/rest`.

To try it, simply `curl` an XML stanza payload:

``` {.sh}
curl https://prosody.example:5281/rest \
    --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \
    -H 'Content-Type: application/xmpp+xml' \
    --data-binary '<message type="chat" to="user@example.org">
            <body>Hello!</body>
        </body>'
```

or a JSON payload:

``` {.sh}
curl https://prosody.example:5281/rest \
    --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \
    -H 'Content-Type: application/json' \
    --data-binary '{
           "body" : "Hello!",
           "kind" : "message",
           "to" : "user@example.org",
           "type" : "chat"
        }'
```

The `Content-Type` header is important!

### Replies

A POST containing an `<iq>` stanza automatically wait for the reply,
long-polling style.

``` {.sh}
curl https://prosody.example:5281/rest \
    --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \
    -H 'Content-Type: application/xmpp+xml' \
    --data-binary '<iq type="get" to="example.net">
            <ping xmlns="urn:xmpp:ping"/>
        </iq>'
```

Replies to other kinds of stanzas that are generated by the same Prosody
instance *MAY* be returned in the HTTP response. Replies from other
entities (connected clients or remote servers) will not be returned, but
can be forwarded via the callback API described in the next section.

## Receiving stanzas

TL;DR: Set this webhook callback URL, get XML `POST`-ed there.

``` {.lua}
Component "rest.example.net" "rest"
rest_credentials = "Bearer dmVyeSBzZWNyZXQgdG9rZW4K"
rest_callback_url = "http://my-api.example:9999/stanzas"
```

To enable JSON payloads set

``` {.lua}
rest_callback_content_type = "application/json"
```

Example callback looks like:

``` {.xml}
POST /stanzas HTTP/1.1
Content-Type: application/xmpp+xml
Content-Length: 102

<message to="bot@rest.example.net" from="user@example.com" type="chat">
<body>Hello</body>
</message>
```

or as JSON:

``` {.json}
POST /stanzas HTTP/1.1
Content-Type: application/json
Content-Length: 133

{
   "body" : "Hello",
   "from" : "user@example.com",
   "kind" : "message",
   "to" : "bot@rest.example.net",
   "type" : "chat"
}
```

### Replying

To accept the stanza without returning a reply, respond with HTTP status
code `202` or `204`.

HTTP status codes in the `4xx` and `5xx` range are mapped to an
appropriate stanza error.

For full control over the response, set the `Content-Type` header to
`application/xmpp+xml` and return an XMPP stanza as an XML snippet.

``` {.xml}
HTTP/1.1 200 Ok
Content-Type: application/xmpp+xml

<message type="chat">
<body>Yes, this is bot</body>
</message>
```

## Payload format

### JSON

``` {.json}
{
   "body" : "Hello!",
   "kind" : "message",
   "type" : "chat"
}
```

Mapping of various XMPP stanza payloads to JSON.

### XML

``` {.xml}
<message type="" id="" to="" from="" xml:lang="">
...
</message>
```

An XML declaration (`<?xml?>`) **MUST NOT** be included.

The payload MUST contain one (1) `message`, `presence` or `iq` stanza.

The stanzas MUST NOT have an `xmlns` attribute, and the default/empty
namespace is treated as `jabber:client`.

# Examples

## Python / Flask

Simple echo bot that responds to messages as XML:

``` {.python}
from flask import Flask, Response, request
import xml.etree.ElementTree as ET

app = Flask("echobot")


@app.before_request
def parse():
    request.stanza = ET.fromstring(request.data)


@app.route("/", methods=["POST"])
def hello():
    if request.stanza.tag == "message":
        return Response(
            "<message><body>Yes this is bot</body></message>",
            content_type="application/xmpp+xml",
        )

    return Response(status=501)


if __name__ == "__main__":
    app.run()
```

And a JSON variant:

``` {.python}
from flask import Flask, Response, request, jsonify

app = Flask("echobot")


@app.route("/", methods=["POST"])
def hello():
    print(request.data)
    if request.is_json:
        data = request.get_json()
        if data["kind"] == "message":
            return jsonify({"body": "hello"})

    return Response(status=501)


if __name__ == "__main__":
    app.run()
```

Remember to set `rest_callback_content_type = "application/json"` for
this to work.

# Compatibility

Requires Prosody trunk / 0.12