github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/docs/intents.md (about)

     1  [Table of contents](README.md#table-of-contents)
     2  
     3  # Intents
     4  
     5  -   [Overview](#overview)
     6  -   [Glossary](#glossary)
     7  -   [Proposal](#proposal)
     8      -   [1. Manifest](#1-manifest)
     9      -   [2. Intent Start](##2-intent-start)
    10      -   [3. Service Resolution](#3-service-resolution)
    11      -   [4. Handshake](#4-handshake)
    12      -   [5. Processing & Terminating](#5-processing--terminating)
    13  -   [Routes](#routes)
    14  -   [Annexes](#annexes)
    15      -   [Use cases](#use-cases)
    16      -   [Bibliography & Prior Art](#bibliography--prior-art)
    17      -   [Discarded Ideas](#discarded-ideas)
    18  
    19  ## Overview
    20  
    21  A typical Cozy Cloud runs multiple applications, but most of these applications
    22  are focused on one task and interact with one particular type of data.
    23  
    24  However, Cozy Cloud especially shines when data is combined across apps to
    25  provide an integrated experience. This is made difficult by the fact that apps
    26  have no dedicated back-end and that they have restricted access to documents.
    27  
    28  This document outlines a proposal for apps to rely on each other to accomplish
    29  certain tasks and gain access to new documents, in a way that hopefully is
    30  neither painful for the users nor the developers.
    31  
    32  ## Glossary
    33  
    34  -   **Intent**: Intents, sometimes also called Activities, is a pattern used in
    35      environments where multiple apps with different purposes coexist. The idea
    36      is that any app can express the need to do _something_ that it can't do
    37      itself, and an app that _can_ do it will take over from there.
    38  -   **Stack**: refers to [cozy-stack](https://github.com/cozy/cozy-stack/), the
    39      server-side part of the Cozy infrastructure.
    40  -   **Client**: the client is the application that _starts_ an intent.
    41  -   **Service**: the service is the application that _handles_ an intent started
    42      by a client.
    43  -   **Available apps**: Uninstalled applications that _handles_ the intent.
    44  
    45  ## Proposal
    46  
    47  In this proposal, services declare themselves through their manifest. When a
    48  clients starts an intent, the stack finds the appropriate service and help the
    49  two apps to create a communication channel between them. After the channel is
    50  ready, the intent is processed. These steps are detailed below.
    51  
    52  ### 1. Manifest
    53  
    54  Every app can register itself as a potential handler for one or more intents. To
    55  do so, it must provide the following information for each intent it wishes to
    56  handle:
    57  
    58  -   `action`: A verb that describes what the service should do. The most common
    59      actions are `CREATE`, `EDIT`, `OPEN`, `PICK`, and `SHARE`. While we
    60      recommend using one of these verbs, the list is not exhaustive and may be
    61      extended by any app.
    62  -   `type`: One or more types of data on which the service knows how to operate
    63      the `action`. A `type` can be expressed as a
    64      [MIME type](https://en.wikipedia.org/wiki/Media_type) or a
    65      [Cozy Document Type](https://github.com/cozy/cozy-stack/blob/master/docs/data-system.md#typing).
    66      The application must have permissions for any Cozy Document Type listed
    67      here. You can also think of the `type` as the intent's subject.
    68  -   `href`: the relative URL of the route designed to handle this intent. A
    69      query-string with the intent id will be added to this URL.
    70  
    71  These informations must be provided in the manifest of the application, inside
    72  the `intents` key.
    73  
    74  Here is a very simple example:
    75  
    76  ```json
    77  "intents": [
    78      {
    79          "action": "PICK",
    80          "type": ["io.cozy.files"],
    81          "href": "/pick"
    82      }
    83  ]
    84  ```
    85  
    86  When this service is called, it will load the page
    87  `https://service.domain.example.com/pick?intent=123abc`.
    88  
    89  Here is an example of an app that supports multiple data types:
    90  
    91  ```json
    92  "intents": [
    93      {
    94          "action": "PICK",
    95          "type": ["io.cozy.files", "image/*"],
    96          "href": "/pick"
    97      }
    98  ]
    99  ```
   100  
   101  Finally, here is an example of an app that supports several intent types:
   102  
   103  ```json
   104  "intents": [
   105      {
   106          "action": "PICK",
   107          "type": ["io.cozy.files", "image/*"],
   108          "href": "/pick"
   109      },
   110      {
   111          "action": "VIEW",
   112          "type": ["image/gif"],
   113          "href": "/viewer"
   114      }
   115  ]
   116  ```
   117  
   118  This information is stored by the stack when the application is installed.
   119  
   120  ### 2. Intent Start
   121  
   122  Any app can start a new intent whenever it wants. When it does, the app becomes
   123  the _client_.
   124  
   125  To start an intent, it must specify the following information:
   126  
   127  -   `action`: an action verb, which will be matched against the actions
   128      declared in services manifest files.
   129  -   `type`: a **single** data type, which will be matched against the types
   130      declared in services manifest files.
   131  
   132  There are also two optional fields that can be defined at the start of the
   133  intent:
   134  
   135  -   `data`: Any data that the client wants to make available to the service.
   136      `data` must be a JSON object but its structure is left to the discretion of
   137      the client. The only exception is when `type` is a MIME type and the client
   138      wishes to send a file to the service. In that case, the file should be
   139      represented as a base-64 encoded [Data URL](http://dataurl.net/#about) and
   140      must be named `content`. This convention is also recomended when dealing
   141      with other intent `type`s. See the examples below for an example of this.
   142  -   `permissions`: When `type` is a Cozy Document Type and the client expects
   143      to receive one or more documents as part of the reply from the service, the
   144      `permissions` field allows the client to request permissions for these
   145      documents. `permissions` is a list of HTTP Verbs. Refer
   146      [to this section](https://github.com/cozy/cozy-stack/blob/master/docs/permissions.md#verbs)
   147      of the permission documentation for more information.
   148  
   149  **Note**: if the intent's subject is a Cozy Doctype that holds references to
   150  other Cozy Documents (such as an album referencing photos or a playlist
   151  referencing music files), the permissions should be granted for the referenced
   152  documents too, whenever possible.
   153  
   154  Here is an example of what the API could look like:
   155  
   156  ```js
   157  // "Let the user pick a file"
   158  cozy.intents.start('PICK', 'io.cozy.files')
   159  .then(document => {
   160      // document is a JSON representation of the picked file
   161  });
   162  
   163  // "Create a contact, with some information already filled out"
   164  cozy.intents.start('CREATE', 'io.cozy.contacts', {
   165      name: 'John Johnsons',
   166      tel: '+12345678'
   167      email: 'john@johnsons.com'
   168  })
   169  .then(document => {
   170      // document is a JSON representation of the contact that was created
   171  });
   172  
   173  // "Save this file somewhere"
   174  cozy.intents.start('CREATE', 'io.cozy.files', {
   175      content: 'data:application/zip;base64,UEsDB...',
   176      name: 'photos.zip'
   177  });
   178  
   179  // "Create a new note, and give me read-only access to it"
   180  cozy.intents.start('CREATE', 'io.cozy.notes', {}, ['GET'])
   181  .then(document => {
   182      // document is a JSON representation of the note that was created.
   183      // Additionally, this note can now be retrieved through the API since we have read access on it.
   184  });
   185  
   186  // "Create an event based on the provided data, and give me full access to it"
   187  cozy.intents.start('CREATE', 'io.cozy.events', {
   188      date: '2017/06/24',
   189      title: 'Beach day'
   190  }, ['ALL'])
   191  .then(document => {
   192      // document is a JSON representation of the note that was created.
   193      // Additionally, this note can now be retrieved through the API since we have read access on it.
   194  });
   195  
   196  // "Crop this picture"
   197  cozy.intents.start('EDIT', 'image/png', {
   198      content: 'data:image/png;base64,iVBORw...',
   199      width: 50,
   200      height: 50
   201  })
   202  .then(image => {
   203      //image is the edited version of the image provided above.
   204  })
   205  ```
   206  
   207  ### 3. Service Resolution
   208  
   209  The service resolution is the phase where a service is chosen to handle an
   210  intent. This phase is handled by the stack.
   211  
   212  After the client has started it's intent, it sends the `action`, the `type` and
   213  the `permissions` to the stack. The stack will then traverse the list of
   214  installed apps and find all the apps that can handle that specific combination.
   215  Note that if the intent request `GET` permissions on a certain Cozy Document
   216  Type, but the service does not have this permission itself, it can not handle
   217  the intent.
   218  
   219  The stack then stores the information for that intent:
   220  
   221  -   A unique id for that intent
   222  -   Client URL
   223  -   Service URL
   224  -   `action`
   225  -   `type`
   226  -   `permissions`
   227  
   228  Finally, the service URL is suffixed with `?intent=` followed by the intent's
   229  id, and then sent to the client.
   230  
   231  ### 4. Available apps
   232  
   233  In addition to the services that manage intents, a list of available (but not
   234  installed) applications of the instance that handles the intent is returned.
   235  This object list is called `availableApps` and contains:
   236  - The application `slug`
   237  - The application `name`
   238  
   239  #### Service choice
   240  
   241  If more than one service match the intent's criteria, the stack returns the list
   242  of all matching service URLs to the client (and stores it in the service URL
   243  version of the intent it keeps in memory). The client is then free to pick one
   244  arbitrarily.
   245  
   246  The client may also decide to let the user choose one of the services. To do
   247  this, it should start another intent with a `PICK` action and a `io.cozy.apps`
   248  `type`. This intent should be resolved by the stack to a special page, in order
   249  to avoid having multiple services trying to handle it and ending up in a loop.
   250  
   251  This special page is a service like any other; it expects the list of services
   252  to pick from as input data, and will return the one that has been picked to the
   253  client. The client can then proceed with the first intent.
   254  
   255  The user may decide to abort the intent before picking a service. If that is the
   256  case, the choice page will need to inform the client that the intent was
   257  aborted.
   258  
   259  #### No available service
   260  
   261  If no service is available to handle an intent, the stack returns an error to
   262  the client. However, some non-installed applications may handle the intent, and
   263  are listed in the `availableApps` field.
   264  
   265  ### 4. Handshake
   266  
   267  The next phase consist of the client and the service establishing a
   268  communication channel between them. The communication will be done using the
   269  [window.postMessage](https://developer.mozilla.org/fr/docs/Web/API/Window/postMessage)
   270  API.
   271  
   272  #### Service Initialization
   273  
   274  When the client receives the service URL from the stack, it starts to listen for
   275  messages coming from that URL. Once the listener is started, it opens an iframe
   276  that points to the service's URL.
   277  
   278  #### Service to Client
   279  
   280  At this point, the service app is opened on the route that it provided in the
   281  `href` part of it's manifest. This route now also contains the intent's id.
   282  
   283  The service queries the stack to find out information about the intent, passing
   284  along the intent id. In response, the stack sends the client's URL, the
   285  `action`, and the `type`. If the intent includes `permissions`, the stack sends
   286  them too, as well as the client's permission id.
   287  
   288  It then starts to listen for messages coming from the client's URL. Eventually,
   289  it sends a message to the client, as a mean to inform it that the service is now
   290  ready.
   291  
   292  #### Client to Service
   293  
   294  After the client receives the "ready" message from the service, it sends a
   295  message to the service acknowledging the "ready" state.
   296  
   297  Along with this message, it should send the `data` that was provided at the
   298  start of the intent, if any.
   299  
   300  ### 5. Processing & Terminating
   301  
   302  After this handshake, there is a confirmed communication channel between the
   303  client and the service, and the service knows what it has to do. This is the
   304  phase where the user interacts with the service.
   305  
   306  If the service is going to grant extra permissions to the client app, it is
   307  strongly recommended to make this clear to the user.
   308  
   309  When the service has finished his task, it sends a "completed" message to the
   310  client. Permissions extensions should have been done before that. Along with the
   311  completed message, the service should send any data it deems relevant. This data
   312  should be a JSON object. Again, the structure of that object is left to the
   313  discretion of the service, except when `type` is a MIME type. In that case, the
   314  file should be represented as a base-64 encoded
   315  [Data URL](http://dataurl.net/#about) and must be named `content`. This
   316  convention is also recomended when dealing with other intent `type`s.
   317  
   318  After the client receives a "completed" message, it can close the service's
   319  iframe and resume operations as usual.
   320  
   321  If, for whatever reason, the service can not fulfill the intent, it can send an
   322  "error" message to the client. When the client receives an "error" message, the
   323  intent is aborted and the iframe can be closed.
   324  
   325  ## Routes
   326  
   327  ### POST /intents
   328  
   329  The client app can ask to start an intent via this route.
   330  
   331  Any client-side app can call this route, no permission is needed.
   332  
   333  #### Request
   334  
   335  ```
   336  POST /intents HTTP/1.1
   337  Host: cozy.example.net
   338  Authorization: Bearer eyJhbG...
   339  Content-Type: application/vnd.api+json
   340  Accept: application/vnd.api+json
   341  ```
   342  
   343  ```json
   344  {
   345      "data": {
   346          "type": "io.cozy.intents",
   347          "attributes": {
   348              "action": "PICK",
   349              "type": "io.cozy.files",
   350              "permissions": ["GET"]
   351          }
   352      }
   353  }
   354  ```
   355  
   356  #### Response
   357  
   358  ```http
   359  HTTP/1.1 201 Created
   360  Content-Type: application/vnd.api+json
   361  ```
   362  
   363  ```json
   364  {
   365      "data": {
   366          "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
   367          "type": "io.cozy.intents",
   368          "attributes": {
   369              "action": "PICK",
   370              "type": "io.cozy.files",
   371              "permissions": ["GET"],
   372              "client": "https://contacts.cozy.example.net",
   373              "services": [
   374                  {
   375                      "slug": "files",
   376                      "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338"
   377                  }
   378              ],
   379              "availableApps": [
   380                  {
   381                      "slug": "myapp",
   382                      "name": "My App"
   383                  }
   384              ]
   385          },
   386          "links": {
   387              "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
   388              "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6"
   389          }
   390      }
   391  }
   392  ```
   393  
   394  ### GET /intents/:id
   395  
   396  Get all the informations about the intent
   397  
   398  **Note**: only the service can access this route (no permission involved).
   399  
   400  #### Request
   401  
   402  ```http
   403  GET /intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338 HTTP/1.1
   404  Host: cozy.example.net
   405  Authorization: Bearer J9l-ZhwP...
   406  Content-Type: application/vnd.api+json
   407  Accept: application/vnd.api+json
   408  ```
   409  
   410  #### Response
   411  
   412  ```http
   413  HTTP/1.1 200 OK
   414  Content-Type: application/vnd.api+json
   415  ```
   416  
   417  ```json
   418  {
   419      "data": {
   420          "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
   421          "type": "io.cozy.intents",
   422          "attributes": {
   423              "action": "PICK",
   424              "type": "io.cozy.files",
   425              "permissions": ["GET"],
   426              "client": "https://contacts.cozy.example.net",
   427              "services": [
   428                  {
   429                      "slug": "files",
   430                      "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338"
   431                  }
   432              ],
   433              "availableApps": [
   434                  {
   435                      "slug": "myapp",
   436                      "name": "My App"
   437                  }
   438              ]
   439          },
   440          "links": {
   441              "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338",
   442              "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6"
   443          }
   444      }
   445  }
   446  ```
   447  
   448  ## Annexes
   449  
   450  ### Use Cases
   451  
   452  Here is a non exhaustive list of situations that _may_ use intents:
   453  
   454  -   Configure a new connector account
   455  -   Share a photo album via email
   456  -   Add an attachment to an email
   457  -   Attach a note to a contact
   458  -   Create a contact based on an email
   459  -   Save an attachment received in an email
   460  -   Create a birthday event in a calendar, based on a contact
   461  -   Create an event based on an email's content
   462  -   Create an event based on an ICS file
   463  -   Chose an avatar for a contact
   464  -   Open a file from the file browser (music, PDF, image, ...)
   465  -   Attach a receipt to an expense
   466  -   Provide a tip for another application
   467  
   468  ### Bibliography & Prior Art
   469  
   470  -   [Prior art](https://forum.cozy.io/t/cozy-tech-topic-inter-app-communication-architecture/2287)
   471  -   [Web intents](http://webintents.org/)
   472  -   [WebActivities](https://wiki.mozilla.org/WebAPI/WebActivities) and [WebActivities on FirefoxOS (archive)](https://developer.mozilla.org/en-US/docs/Archive/Firefox_OS/API/Web_Activities)
   473  -   [Siri Intents](https://developer.apple.com/documentation/sirikit#//apple_ref/doc/uid/TP40016875-CH5-SW1)
   474  -   [Android Intents](https://developer.android.com/reference/android/content/Intent.html)
   475  -   [iOS extensions](https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/index.html)
   476  
   477  ### Discarded Ideas
   478  
   479  #### Disposition
   480  
   481  Some specifications include a `disposition` field in the manifests, that gives a
   482  hint to the client about how to display the service (inlined or in a new
   483  window). Since we were unable to find a use case where a new window is required,
   484  we decided not to include the `disposition` in this specification. It might be
   485  added later if the need arises.
   486  
   487  #### Client / Server architecture
   488  
   489  Instead of letting the two applications communicate with each other, they could
   490  be made to talk through the stack. This would be useful as the stack could act
   491  as a middleware, transforming data on the fly or granting permissions where
   492  appropriate.
   493  
   494  However, this approach has also severe drawbacks, notably the fact that the
   495  stack must hold a copy of all the data that the apps want to transfer (which can
   496  include large files). It also makes it significantly harder for the client to
   497  know when the intent has been processed.
   498  
   499  #### Data representation
   500  
   501  The client could explicitly request a data format to be used in the
   502  communication. However, this idea was abandoned because the format is _always_
   503  json, except when then intent's `type` is a MIME type, in which case the data
   504  format _also_ uses this type.