github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/intent/intents.go (about) 1 package intent 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "path" 10 "strings" 11 12 "github.com/cozy/cozy-stack/model/app" 13 "github.com/cozy/cozy-stack/model/instance" 14 "github.com/cozy/cozy-stack/pkg/consts" 15 "github.com/cozy/cozy-stack/pkg/couchdb" 16 "github.com/cozy/cozy-stack/pkg/registry" 17 "github.com/cozy/cozy-stack/pkg/utils" 18 ) 19 20 // Service is a struct for an app that can serve an intent 21 type Service struct { 22 Slug string `json:"slug"` 23 Href string `json:"href"` 24 } 25 26 // AvailableApp is a struct for the apps that are in the apps registry but not 27 // installed, and can be used for the intent. 28 type AvailableApp struct { 29 Slug string `json:"slug"` 30 Name string `json:"name"` 31 } 32 33 // Intent is a struct for a call from a client-side app to have another app do 34 // something for it 35 type Intent struct { 36 IID string `json:"_id,omitempty"` 37 IRev string `json:"_rev,omitempty"` 38 Action string `json:"action"` 39 Type string `json:"type"` 40 Permissions []string `json:"permissions"` 41 Client string `json:"client"` 42 Services []Service `json:"services"` 43 AvailableApps []AvailableApp `json:"availableApps"` 44 } 45 46 // ID is used to implement the couchdb.Doc interface 47 func (in *Intent) ID() string { return in.IID } 48 49 // Rev is used to implement the couchdb.Doc interface 50 func (in *Intent) Rev() string { return in.IRev } 51 52 // DocType is used to implement the couchdb.Doc interface 53 func (in *Intent) DocType() string { return consts.Intents } 54 55 // Clone implements couchdb.Doc 56 func (in *Intent) Clone() couchdb.Doc { 57 cloned := *in 58 cloned.Permissions = make([]string, len(in.Permissions)) 59 copy(cloned.Permissions, in.Permissions) 60 cloned.Services = make([]Service, len(in.Services)) 61 copy(cloned.Services, in.Services) 62 cloned.AvailableApps = make([]AvailableApp, len(in.AvailableApps)) 63 copy(cloned.AvailableApps, in.AvailableApps) 64 return &cloned 65 } 66 67 // SetID is used to implement the couchdb.Doc interface 68 func (in *Intent) SetID(id string) { in.IID = id } 69 70 // SetRev is used to implement the couchdb.Doc interface 71 func (in *Intent) SetRev(rev string) { in.IRev = rev } 72 73 // Save will persist the intent in CouchDB 74 func (in *Intent) Save(instance *instance.Instance) error { 75 if in.ID() != "" { 76 return couchdb.UpdateDoc(instance, in) 77 } 78 return couchdb.CreateDoc(instance, in) 79 } 80 81 // GenerateHref creates the href where the service can be called for an intent 82 func (in *Intent) GenerateHref(instance *instance.Instance, slug, target string) string { 83 u := instance.SubDomain(slug) 84 parts := strings.SplitN(target, "#", 2) 85 if len(parts[0]) > 0 { 86 u.Path = parts[0] 87 } 88 if len(parts) == 2 && len(parts[1]) > 0 { 89 u.Fragment = parts[1] 90 } 91 u.RawQuery = "intent=" + in.ID() 92 return u.String() 93 } 94 95 // FillServices looks at all the application that can answer this intent 96 // and save them in the services field 97 func (in *Intent) FillServices(instance *instance.Instance) error { 98 res, _, err := app.ListWebappsWithPagination(instance, 0, "") 99 if err != nil { 100 return err 101 } 102 for _, man := range res { 103 if intent := man.FindIntent(in.Action, in.Type); intent != nil { 104 href := in.GenerateHref(instance, man.Slug(), intent.Href) 105 service := Service{Slug: man.Slug(), Href: href} 106 in.Services = append(in.Services, service) 107 } 108 } 109 return nil 110 } 111 112 type jsonAPIWebapp struct { 113 Data []*app.WebappManifest `json:"data"` 114 Count int `json:"count"` 115 } 116 117 // GetInstanceWebapps returns the list of available webapps for the instance by 118 // iterating over its registries 119 func GetInstanceWebapps(inst *instance.Instance) ([]string, error) { 120 man := jsonAPIWebapp{} 121 apps := []string{} 122 123 for _, regURL := range inst.Registries() { 124 url, err := url.Parse(regURL.String()) 125 if err != nil { 126 return nil, err 127 } 128 url.Path = path.Join(url.Path, "registry") 129 url.RawQuery = "filter[type]=webapp" 130 131 req, err := http.NewRequest("GET", url.String(), nil) 132 if err != nil { 133 return nil, err 134 } 135 res, err := http.DefaultClient.Do(req) 136 if err != nil { 137 return nil, err 138 } 139 140 err = json.NewDecoder(res.Body).Decode(&man) 141 if err != nil { 142 return nil, err 143 } 144 145 for _, app := range man.Data { 146 slug := app.Slug() 147 if !utils.IsInArray(slug, apps) { 148 apps = append(apps, slug) 149 } 150 } 151 } 152 153 return apps, nil 154 } 155 156 // FillAvailableWebapps finds webapps which can answer to the intent from 157 // non-installed instance webapps 158 func (in *Intent) FillAvailableWebapps(inst *instance.Instance) error { 159 // Webapps to exclude 160 installedWebApps, _, err := app.ListWebappsWithPagination(inst, 0, "") 161 if err != nil { 162 return err 163 } 164 165 endSlugs := []string{} 166 webapps, err := GetInstanceWebapps(inst) 167 if err != nil { 168 return err 169 } 170 // Only appending the non-installed webapps 171 for _, wa := range webapps { 172 found := false 173 for _, iwa := range installedWebApps { 174 if wa == iwa.Slug() { 175 found = true 176 break 177 } 178 } 179 if !found { 180 endSlugs = append(endSlugs, wa) 181 } 182 } 183 184 lastVersions := map[string]app.WebappManifest{} 185 versionsChan := make(chan app.WebappManifest) 186 errorsChan := make(chan error) 187 188 registries := inst.Registries() 189 for _, webapp := range endSlugs { 190 go func(webapp string) { 191 webappMan := app.WebappManifest{} 192 v, err := registry.GetLatestVersion(webapp, "stable", registries) 193 if err != nil { 194 errorsChan <- fmt.Errorf("Could not get last version for %s: %s", webapp, err) 195 return 196 } 197 err = json.NewDecoder(bytes.NewReader(v.Manifest)).Decode(&webappMan) 198 if err != nil { 199 errorsChan <- fmt.Errorf("Could not get decode manifest for %s: %s", webapp, err) 200 return 201 } 202 203 versionsChan <- webappMan 204 }(webapp) 205 } 206 207 for range endSlugs { 208 select { 209 case err := <-errorsChan: 210 inst.Logger().WithNamespace("intents").Error(err.Error()) 211 case version := <-versionsChan: 212 lastVersions[version.Slug()] = version 213 } 214 } 215 close(versionsChan) 216 close(errorsChan) 217 218 for _, manif := range lastVersions { 219 if intent := manif.FindIntent(in.Action, in.Type); intent != nil { 220 availableApp := AvailableApp{ 221 Name: manif.Name(), 222 Slug: manif.Slug(), 223 } 224 in.AvailableApps = append(in.AvailableApps, availableApp) 225 } 226 } 227 228 return nil 229 } 230 231 var _ couchdb.Doc = (*Intent)(nil)