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)