Table of contents

Intents

Overview

A typical Cozy Cloud runs multiple applications, but most of these applications are focused on one task and interact with one particular type of data.

However, Cozy Cloud especially shines when data is combined across apps to provide an integrated experience. This is made difficult by the fact that apps have no dedicated back-end and that they have restricted access to documents.

This document outlines a proposal for apps to rely on each other to accomplish certain tasks and gain access to new documents, in a way that hopefully is neither painful for the users nor the developers.

Glossary

Proposal

In this proposal, services declare themselves through their manifest. When a clients starts an intent, the stack finds the appropriate service and help the two apps to create a communication channel between them. After the channel is ready, the intent is processed. These steps are detailed below.

1. Manifest

Every app can register itself as a potential handler for one or more intents. To do so, it must provide the following information for each intent it wishes to handle:

These informations must be provided in the manifest of the application, inside the intents key.

Here is a very simple example:

"intents": [
    {
        "action": "PICK",
        "type": ["io.cozy.files"],
        "href": "/pick"
    }
]

When this service is called, it will load the page https://service.domain.example.com/pick?intent=123abc.

Here is an example of an app that supports multiple data types:

"intents": [
    {
        "action": "PICK",
        "type": ["io.cozy.files", "image/*"],
        "href": "/pick"
    }
]

Finally, here is an example of an app that supports several intent types:

"intents": [
    {
        "action": "PICK",
        "type": ["io.cozy.files", "image/*"],
        "href": "/pick"
    },
    {
        "action": "VIEW",
        "type": ["image/gif"],
        "href": "/viewer"
    }
]

This information is stored by the stack when the application is installed.

2. Intent Start

Any app can start a new intent whenever it wants. When it does, the app becomes the client.

To start an intent, it must specify the following information:

There are also two optional fields that can be defined at the start of the intent:

Note: if the intent’s subject is a Cozy Doctype that holds references to other Cozy Documents (such as an album referencing photos or a playlist referencing music files), the permissions should be granted for the referenced documents too, whenever possible.

Here is an example of what the API could look like:

// "Let the user pick a file"
cozy.intents.start('PICK', 'io.cozy.files')
.then(document => {
    // document is a JSON representation of the picked file
});

// "Create a contact, with some information already filled out"
cozy.intents.start('CREATE', 'io.cozy.contacts', {
    name: 'John Johnsons',
    tel: '+12345678'
    email: 'john@johnsons.com'
})
.then(document => {
    // document is a JSON representation of the contact that was created
});

// "Save this file somewhere"
cozy.intents.start('CREATE', 'io.cozy.files', {
    content: 'data:application/zip;base64,UEsDB...',
    name: 'photos.zip'
});

// "Create a new note, and give me read-only access to it"
cozy.intents.start('CREATE', 'io.cozy.notes', {}, ['GET'])
.then(document => {
    // document is a JSON representation of the note that was created.
    // Additionally, this note can now be retrieved through the API since we have read access on it.
});

// "Create an event based on the provided data, and give me full access to it"
cozy.intents.start('CREATE', 'io.cozy.events', {
    date: '2017/06/24',
    title: 'Beach day'
}, ['ALL'])
.then(document => {
    // document is a JSON representation of the note that was created.
    // Additionally, this note can now be retrieved through the API since we have read access on it.
});

// "Crop this picture"
cozy.intents.start('EDIT', 'image/png', {
    content: 'data:image/png;base64,iVBORw...',
    width: 50,
    height: 50
})
.then(image => {
    //image is the edited version of the image provided above.
})

3. Service Resolution

The service resolution is the phase where a service is chosen to handle an intent. This phase is handled by the stack.

After the client has started it’s intent, it sends the action, the type and the permissions to the stack. The stack will then traverse the list of installed apps and find all the apps that can handle that specific combination. Note that if the intent request GET permissions on a certain Cozy Document Type, but the service does not have this permission itself, it can not handle the intent.

The stack then stores the information for that intent:

Finally, the service URL is suffixed with ?intent= followed by the intent’s id, and then sent to the client.

Service choice

If more than one service match the intent’s criteria, the stack returns the list of all matching service URLs to the client (and stores it in the service URL version of the intent it keeps in memory). The client is then free to pick one arbitrarily.

The client may also decide to let the user choose one of the services. To do this, it should start another intent with a PICK action and a io.cozy.apps type. This intent should be resolved by the stack to a special page, in order to avoid having multiple services trying to handle it and ending up in a loop.

This special page is a service like any other; it expects the list of services to pick from as input data, and will return the one that has been picked to the client. The client can then proceed with the first intent.

The user may decide to abort the intent before picking a service. If that is the case, the choice page will need to inform the client that the intent was aborted.

No available service

If no service is available to handle an intent, the stack returns an error to the client.

At a later phase of this project, the stack may traverse the applications registered in a store to find suitable services, and prompt the user to install one.

4. Handshake

The next phase consist of the client and the service establishing a communication channel between them. The communication will be done using the window.postMessage API.

Service Initialization

When the client receives the service URL from the stack, it starts to listen for messages coming from that URL. Once the listener is started, it opens an iframe that points to the service’s URL.

Service to Client

At this point, the service app is opened on the route that it provided in the href part of it’s manifest. This route now also contains the intent’s id.

The service queries the stack to find out information about the intent, passing along the intent id. In response, the stack sends the client’s URL, the action, and the type. If the intent includes permissions, the stack sends them too, as well as the client’s permission id.

It then starts to listen for messages coming from the client’s URL. Eventually, it sends a message to the client, as a mean to inform it that the service is now ready.

Client to Service

After the client receives the “ready” message from the service, it sends a message to the service acknowledging the “ready” state.

Along with this message, it should send the data that was provided at the start of the intent, if any.

5. Processing & Terminating

After this handshake, there is a confirmed communication channel between the client and the service, and the service knows what it has to do. This is the phase where the user interacts with the service.

If the service is going to grant extra permissions to the client app, it is strongly recommended to make this clear to the user.

When the service has finished his task, it sends a “completed” message to the client. Permissions extensions should have been done before that. Along with the completed message, the service should send any data it deems relevant. This data should be a JSON object. Again, the structure of that object is left to the discretion of the service, except when type is a MIME type. In that case, the file should be represented as a base-64 encoded Data URL and must be named content. This convention is also recomended when dealing with other intent types.

After the client receives a “completed” message, it can close the service’s iframe and resume operations as usual.

If, for whatever reason, the service can not fulfill the intent, it can send an “error” message to the client. When the client receives an “error” message, the intent is aborted and the iframe can be closed.

Routes

POST /intents

The client app can ask to start an intent via this route.

Any client-side app can call this route, no permission is needed.

Request

POST /intents HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbG...
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
    "data": {
        "type": "io.cozy.intents",
        "attributes": {
            "action": "PICK",
            "type": "io.cozy.files",
            "permissions": ["GET"]
        }
    }
}

Response

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
    "data": {
        "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
        "type": "io.cozy.intents",
        "attributes": {
            "action": "PICK",
            "type": "io.cozy.files",
            "permissions": ["GET"],
            "client": "https://contacts.cozy.example.net",
            "services": [
                {
                    "slug": "files",
                    "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338"
                }
            ]
        },
        "links": {
            "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
            "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6"
        }
    }
}

GET /intents/:id

Get all the informations about the intent

Note: only the service can access this route (no permission involved).

Request

GET /intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338 HTTP/1.1
Host: cozy.example.net
Authorization: Bearer J9l-ZhwP...
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

Response

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
    "data": {
        "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
        "type": "io.cozy.intents",
        "attributes": {
            "action": "PICK",
            "type": "io.cozy.files",
            "permissions": ["GET"],
            "client": "https://contacts.cozy.example.net",
            "services": [
                {
                    "slug": "files",
                    "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338"
                }
            ]
        },
        "links": {
            "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
            "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6"
        }
    }
}

Annexes

Use Cases

Here is a non exhaustive list of situations that may use intents:

Bibliography & Prior Art

Discarded Ideas

Disposition

Some specifications include a disposition field in the manifests, that gives a hint to the client about how to display the service (inlined or in a new window). Since we were unable to find a use case where a new window is required, we decided not to include the disposition in this specification. It might be added later if the need arises.

Client / Server architecture

Instead of letting the two applications communicate with each other, they could be made to talk through the stack. This would be useful as the stack could act as a middleware, transforming data on the fly or granting permissions where appropriate.

However, this approach has also severe drawbacks, notably the fact that the stack must hold a copy of all the data that the apps want to transfer (which can include large files). It also makes it significantly harder for the client to know when the intent has been processed.

Data representation

The client could explicitly request a data format to be used in the communication. However, this idea was abandoned because the format is always json, except when then intent’s type is a MIME type, in which case the data format also uses this type.