1package plugins
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8
9 "github.com/floatpane/matcha/internal/httpclient"
10)
11
12const RegistryURL = "https://raw.githubusercontent.com/floatpane/matcha/master/plugins/registry.json"
13const RawPluginBaseURL = "https://raw.githubusercontent.com/floatpane/matcha/master/plugins/"
14
15// PluginEntry represents a single plugin in the registry.
16type PluginEntry struct {
17 Name string `json:"name"`
18 Title string `json:"title"`
19 Description string `json:"description"`
20 File string `json:"file"`
21 URL string `json:"url,omitempty"`
22}
23
24// FetchRegistry fetches the plugin registry from GitHub.
25func FetchRegistry() ([]PluginEntry, error) {
26 client := httpclient.New(httpclient.RegistryFetchTimeout)
27 resp, err := client.Get(RegistryURL) //nolint:noctx
28 if err != nil {
29 return nil, fmt.Errorf("failed to fetch registry: %w", err)
30 }
31 defer resp.Body.Close() //nolint:errcheck
32
33 if resp.StatusCode != http.StatusOK {
34 return nil, fmt.Errorf("registry returned status %d", resp.StatusCode)
35 }
36
37 body, err := io.ReadAll(resp.Body)
38 if err != nil {
39 return nil, fmt.Errorf("failed to read registry: %w", err)
40 }
41
42 var entries []PluginEntry
43 if err := json.Unmarshal(body, &entries); err != nil {
44 return nil, fmt.Errorf("failed to parse registry: %w", err)
45 }
46 return entries, nil
47}
48
49// FetchPlugin downloads a plugin file. If the entry has a URL, it downloads
50// from there; otherwise it falls back to the default repo location.
51func FetchPlugin(entry PluginEntry) ([]byte, error) {
52 url := entry.URL
53 if url == "" {
54 url = RawPluginBaseURL + entry.File
55 }
56
57 client := httpclient.New(httpclient.RegistryFetchTimeout)
58 resp, err := client.Get(url) //nolint:noctx
59 if err != nil {
60 return nil, fmt.Errorf("failed to fetch plugin: %w", err)
61 }
62 defer resp.Body.Close() //nolint:errcheck
63
64 if resp.StatusCode != http.StatusOK {
65 return nil, fmt.Errorf("plugin download returned status %d", resp.StatusCode)
66 }
67
68 return io.ReadAll(resp.Body)
69}