Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

1// Package radicle talks to one local Radicle node: reads go through 2// radicle-httpd's JSON API (schema confirmed against a live v0.28.0 node — 3// see the field comments below), writes go through the "rad" CLI since 4// radicle-httpd exposes no write endpoints. 5package radicle 6 7import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 "os" 13 "os/exec" 14 "time" 15) 16 17// Client operates on one repository (identified by its RID) tracked by one 18// local Radicle node. 19type Client struct { 20 httpdBaseURL string // e.g. https://r1.cyberwild.org, used for reads 21 rid string 22 radHome string // RAD_HOME for the "rad" CLI, used for writes 23 radBin string // path to the rad binary 24 http *http.Client 25} 26 27func New(httpdBaseURL, rid, radHome, radBin string) *Client { 28 return &Client{ 29 httpdBaseURL: httpdBaseURL, 30 rid: rid, 31 radHome: radHome, 32 radBin: radBin, 33 http: &http.Client{Timeout: 30 * time.Second}, 34 } 35} 36 37func (c *Client) getJSON(path string, out any) error { 38 resp, err := c.http.Get(c.httpdBaseURL + "/api/v1/repos/" + c.rid + path) 39 if err != nil { 40 return fmt.Errorf("GET %s: %w", path, err) 41 } 42 defer resp.Body.Close() 43 if resp.StatusCode >= 300 { 44 return fmt.Errorf("GET %s: HTTP %d", path, resp.StatusCode) 45 } 46 return json.NewDecoder(resp.Body).Decode(out) 47} 48 49// rad runs the CLI against this client's repository and RAD_HOME, returning 50// combined stdout+stderr for logging on error. 51func (c *Client) rad(args ...string) (string, error) { 52 args = append(args, "--repo", c.rid) 53 cmd := exec.Command(c.radBin, args...) 54 cmd.Env = append(os.Environ(), "RAD_HOME="+c.radHome) 55 var out bytes.Buffer 56 cmd.Stdout = &out 57 cmd.Stderr = &out 58 if err := cmd.Run(); err != nil { 59 return out.String(), fmt.Errorf("rad %v: %w: %s", args, err, out.String()) 60 } 61 return out.String(), nil 62} 63 64// Actor identifies who authored an issue, comment, or patch revision. 65type Actor struct { 66 ID string `json:"id"` // did:key:... 67 Alias string `json:"alias"` 68} 69 70// IssueState mirrors radicle-httpd's issue state object. 71type IssueState struct { 72 Status string `json:"status"` // "open" or "closed" 73} 74 75// Issue is the subset of radicle-httpd's issue JSON the daemon needs. 76// Verified against GET /api/v1/repos/:rid/issues?status=all on httpd 0.28.0. 77type Issue struct { 78 ID string `json:"id"` 79 Title string `json:"title"` 80 State IssueState `json:"state"` 81 Author Actor `json:"author"` 82 Discussion []struct { 83 Body string `json:"body"` 84 } `json:"discussion"` 85} 86 87// Body returns the issue's opening comment, if any. 88func (i Issue) Body() string { 89 if len(i.Discussion) == 0 { 90 return "" 91 } 92 return i.Discussion[0].Body 93} 94 95func (c *Client) ListIssues() ([]Issue, error) { 96 var issues []Issue 97 if err := c.getJSON("/issues?status=all", &issues); err != nil { 98 return nil, err 99 } 100 return issues, nil 101} 102 103// OpenIssue creates a new issue via the CLI (no write API exists on httpd). 104func (c *Client) OpenIssue(title, description string) (string, error) { 105 out, err := c.rad("issue", "open", "-t", title, "-d", description, "--quiet") 106 if err != nil { 107 return "", err 108 } 109 // --quiet still prints the new issue id on its own line. 110 id := trimLastLine(out) 111 if id == "" { 112 return "", fmt.Errorf("rad issue open: could not parse issue id from output: %q", out) 113 } 114 return id, nil 115} 116 117// PatchState mirrors radicle-httpd's patch state object. 118type PatchState struct { 119 Status string `json:"status"` // "open", "draft", "archived", "merged" 120} 121 122// Patch is the subset of radicle-httpd's patch JSON the daemon needs. 123// Verified against GET /api/v1/repos/:rid/patches?status=all on httpd 0.28.0. 124type Patch struct { 125 ID string `json:"id"` 126 Title string `json:"title"` 127 State PatchState `json:"state"` 128 Author Actor `json:"author"` 129 Revisions []struct { 130 Description string `json:"description"` 131 OID string `json:"oid"` // head commit of the revision 132 Base string `json:"base"` // commit the patch is based on 133 } `json:"revisions"` 134} 135 136func (c *Client) ListPatches() ([]Patch, error) { 137 var patches []Patch 138 if err := c.getJSON("/patches?status=all", &patches); err != nil { 139 return nil, err 140 } 141 return patches, nil 142} 143 144func trimLastLine(s string) string { 145 start := len(s) 146 for start > 0 && s[start-1] == '\n' { 147 start-- 148 } 149 end := start 150 for start > 0 && s[start-1] != '\n' { 151 start-- 152 } 153 return s[start:end] 154}