github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/apps.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 "strconv" 11 "time" 12 13 "github.com/cozy/cozy-stack/client/request" 14 "github.com/cozy/cozy-stack/pkg/consts" 15 ) 16 17 // AppManifest holds the JSON-API representation of an application. 18 type AppManifest struct { 19 ID string `json:"id"` 20 Rev string `json:"rev"` 21 Attrs struct { 22 Name string `json:"name"` 23 NamePrefix string `json:"name_prefix,omitempty"` 24 Editor string `json:"editor"` 25 Icon string `json:"icon"` 26 27 Type string `json:"type,omitempty"` 28 License string `json:"license,omitempty"` 29 Language string `json:"language,omitempty"` 30 Category string `json:"category,omitempty"` 31 VendorLink interface{} `json:"vendor_link"` // can be a string or []string 32 Locales *json.RawMessage `json:"locales,omitempty"` 33 Langs *json.RawMessage `json:"langs,omitempty"` 34 Platforms *json.RawMessage `json:"platforms,omitempty"` 35 Categories *json.RawMessage `json:"categories,omitempty"` 36 Developer *json.RawMessage `json:"developer,omitempty"` 37 Screenshots *json.RawMessage `json:"screenshots,omitempty"` 38 Tags *json.RawMessage `json:"tags,omitempty"` 39 40 Frequency string `json:"frequency,omitempty"` 41 DataTypes *json.RawMessage `json:"data_types,omitempty"` 42 Doctypes *json.RawMessage `json:"doctypes,omitempty"` 43 Fields *json.RawMessage `json:"fields,omitempty"` 44 Folders *json.RawMessage `json:"folders,omitempty"` 45 Messages *json.RawMessage `json:"messages,omitempty"` 46 OAuth *json.RawMessage `json:"oauth,omitempty"` 47 TimeInterval *json.RawMessage `json:"time_interval,omitempty"` 48 ClientSide bool `json:"clientSide,omitempty"` 49 50 Slug string `json:"slug"` 51 State string `json:"state"` 52 Source string `json:"source"` 53 Version string `json:"version"` 54 Permissions *map[string]struct { 55 Type string `json:"type"` 56 Description string `json:"description,omitempty"` 57 Verbs []string `json:"verbs,omitempty"` 58 Selector string `json:"selector,omitempty"` 59 Values []string `json:"values,omitempty"` 60 } `json:"permissions"` 61 AvailableVersion string `json:"available_version,omitempty"` 62 63 Parameters json.RawMessage `json:"parameters,omitempty"` 64 65 Intents []struct { 66 Action string `json:"action"` 67 Types []string `json:"type"` 68 Href string `json:"href"` 69 } `json:"intents"` 70 71 Routes *map[string]struct { 72 Folder string `json:"folder"` 73 Index string `json:"index"` 74 Public bool `json:"public"` 75 } `json:"routes,omitempty"` 76 77 Services *map[string]struct { 78 Type string `json:"type"` 79 File string `json:"file"` 80 Debounce string `json:"debounce"` 81 TriggerOptions string `json:"trigger"` 82 TriggerID string `json:"trigger_id"` 83 } `json:"services"` 84 Notifications map[string]struct { 85 Description string `json:"description,omitempty"` 86 Collapsible bool `json:"collapsible,omitempty"` 87 Multiple bool `json:"multiple,omitempty"` 88 Stateful bool `json:"stateful,omitempty"` 89 DefaultPriority string `json:"default_priority,omitempty"` 90 TimeToLive time.Duration `json:"time_to_live,omitempty"` 91 Templates map[string]string `json:"templates,omitempty"` 92 MinInterval time.Duration `json:"min_interval,omitempty"` 93 } `json:"notifications,omitempty"` 94 95 CreatedAt time.Time `json:"created_at"` 96 UpdatedAt time.Time `json:"updated_at"` 97 98 Error string `json:"error,omitempty"` 99 } `json:"attributes,omitempty"` 100 } 101 102 // AppOptions holds the options to install an application. 103 type AppOptions struct { 104 AppType string 105 Slug string 106 SourceURL string 107 Deactivated bool 108 OverridenParameters *json.RawMessage 109 } 110 111 // ListApps is used to get the list of all installed applications. 112 func (c *Client) ListApps(appType string) ([]*AppManifest, error) { 113 res, err := c.Req(&request.Options{ 114 Method: "GET", 115 Path: makeAppsPath(appType, ""), 116 }) 117 if err != nil { 118 return nil, err 119 } 120 var mans []*AppManifest 121 if err := readJSONAPI(res.Body, &mans); err != nil { 122 return nil, err 123 } 124 return mans, nil 125 } 126 127 // GetApp is used to fetch an application manifest with specified slug 128 func (c *Client) GetApp(opts *AppOptions) (*AppManifest, error) { 129 res, err := c.Req(&request.Options{ 130 Method: "GET", 131 Path: makeAppsPath(opts.AppType, url.PathEscape(opts.Slug)), 132 }) 133 if err != nil { 134 return nil, err 135 } 136 return readAppManifest(res) 137 } 138 139 // InstallApp is used to install an application. 140 func (c *Client) InstallApp(opts *AppOptions) (*AppManifest, error) { 141 q := url.Values{ 142 "Source": {opts.SourceURL}, 143 "Deactivated": {strconv.FormatBool(opts.Deactivated)}, 144 } 145 if opts.OverridenParameters != nil { 146 b, err := json.Marshal(opts.OverridenParameters) 147 if err != nil { 148 return nil, err 149 } 150 q["Parameters"] = []string{string(b)} 151 } 152 res, err := c.Req(&request.Options{ 153 Method: "POST", 154 Path: makeAppsPath(opts.AppType, url.PathEscape(opts.Slug)), 155 Queries: q, 156 Headers: request.Headers{ 157 "Accept": "text/event-stream", 158 }, 159 }) 160 if err != nil { 161 return nil, err 162 } 163 return readAppManifestStream(res) 164 } 165 166 // UpdateApp is used to update an application. 167 func (c *Client) UpdateApp(opts *AppOptions, safe bool) (*AppManifest, error) { 168 q := url.Values{ 169 "Source": {opts.SourceURL}, 170 "PermissionsAcked": {strconv.FormatBool(!safe)}, 171 } 172 if opts.OverridenParameters != nil { 173 b, err := json.Marshal(opts.OverridenParameters) 174 if err != nil { 175 return nil, err 176 } 177 q["Parameters"] = []string{string(b)} 178 } 179 res, err := c.Req(&request.Options{ 180 Method: "PUT", 181 Path: makeAppsPath(opts.AppType, url.PathEscape(opts.Slug)), 182 Queries: q, 183 Headers: request.Headers{ 184 "Accept": "text/event-stream", 185 }, 186 }) 187 if err != nil { 188 return nil, err 189 } 190 return readAppManifestStream(res) 191 } 192 193 // UninstallApp is used to uninstall an application. 194 func (c *Client) UninstallApp(opts *AppOptions) (*AppManifest, error) { 195 res, err := c.Req(&request.Options{ 196 Method: "DELETE", 197 Path: makeAppsPath(opts.AppType, url.PathEscape(opts.Slug)), 198 }) 199 if err != nil { 200 return nil, err 201 } 202 return readAppManifest(res) 203 } 204 205 // ListMaintenances returns a list of konnectors in maintenance 206 func (ac *AdminClient) ListMaintenances(context string) ([]interface{}, error) { 207 queries := url.Values{} 208 if context != "" { 209 queries.Add("Context", context) 210 } 211 res, err := ac.Req(&request.Options{ 212 Method: "GET", 213 Path: "/konnectors/maintenance", 214 Queries: queries, 215 }) 216 if err != nil { 217 return nil, err 218 } 219 var list []interface{} 220 if err := readJSONAPI(res.Body, &list); err != nil { 221 return nil, err 222 } 223 return list, nil 224 } 225 226 // ActivateMaintenance is used to activate the maintenance for a konnector 227 func (ac *AdminClient) ActivateMaintenance(slug string, opts map[string]interface{}) error { 228 data := map[string]interface{}{"attributes": opts} 229 body, err := writeJSONAPI(data) 230 if err != nil { 231 return err 232 } 233 _, err = ac.Req(&request.Options{ 234 Method: "PUT", 235 Path: "/konnectors/maintenance/" + slug, 236 Body: body, 237 NoResponse: true, 238 }) 239 return err 240 } 241 242 // DeactivateMaintenance is used to deactivate the maintenance for a konnector 243 func (ac *AdminClient) DeactivateMaintenance(slug string) error { 244 _, err := ac.Req(&request.Options{ 245 Method: "DELETE", 246 Path: "/konnectors/maintenance/" + slug, 247 NoResponse: true, 248 }) 249 return err 250 } 251 252 func makeAppsPath(appType, path string) string { 253 switch appType { 254 case consts.Apps: 255 return "/apps/" + path 256 case consts.Konnectors: 257 return "/konnectors/" + path 258 } 259 panic(fmt.Errorf("Unknown application type %s", appType)) 260 } 261 262 func readAppManifestStream(res *http.Response) (*AppManifest, error) { 263 evtch := make(chan *request.SSEEvent) 264 go request.ReadSSE(res.Body, evtch) 265 var lastevt *request.SSEEvent 266 // get the last sent event 267 for evt := range evtch { 268 if evt.Error != nil { 269 return nil, evt.Error 270 } 271 if evt.Name == "error" { 272 var stringError string 273 if err := json.Unmarshal(evt.Data, &stringError); err != nil { 274 return nil, fmt.Errorf("Could not parse error from event-stream: %s", err.Error()) 275 } 276 return nil, errors.New(stringError) 277 } 278 lastevt = evt 279 } 280 if lastevt == nil { 281 return nil, errors.New("No application data was sent") 282 } 283 app := &AppManifest{} 284 if err := readJSONAPI(bytes.NewReader(lastevt.Data), &app); err != nil { 285 return nil, err 286 } 287 return app, nil 288 } 289 290 func readAppManifest(res *http.Response) (*AppManifest, error) { 291 app := &AppManifest{} 292 if err := readJSONAPI(res.Body, &app); err != nil { 293 return nil, err 294 } 295 return app, nil 296 }