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