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.