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  }