1package knotclient
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "net/url"
11 "strconv"
12 "time"
13
14 "tangled.sh/tangled.sh/core/types"
15)
16
17type UnsignedClient struct {
18 Url *url.URL
19 client *http.Client
20}
21
22func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
23 client := &http.Client{
24 Timeout: 5 * time.Second,
25 }
26
27 scheme := "https"
28 if dev {
29 scheme = "http"
30 }
31 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
32 if err != nil {
33 return nil, err
34 }
35
36 unsignedClient := &UnsignedClient{
37 client: client,
38 Url: url,
39 }
40
41 return unsignedClient, nil
42}
43
44func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
45 reqUrl := us.Url.JoinPath(endpoint)
46
47 // add query parameters
48 if query != nil {
49 reqUrl.RawQuery = query.Encode()
50 }
51
52 return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
53}
54
55func do[T any](us *UnsignedClient, req *http.Request) (*T, error) {
56 resp, err := us.client.Do(req)
57 if err != nil {
58 return nil, err
59 }
60 defer resp.Body.Close()
61
62 body, err := io.ReadAll(resp.Body)
63 if err != nil {
64 log.Printf("Error reading response body: %v", err)
65 return nil, err
66 }
67
68 var result T
69 err = json.Unmarshal(body, &result)
70 if err != nil {
71 log.Printf("Error unmarshalling response body: %v", err)
72 return nil, err
73 }
74
75 return &result, nil
76}
77
78func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*types.RepoIndexResponse, error) {
79 const (
80 Method = "GET"
81 )
82
83 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
84 if ref == "" {
85 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
86 }
87
88 req, err := us.newRequest(Method, endpoint, nil, nil)
89 if err != nil {
90 return nil, err
91 }
92
93 return do[types.RepoIndexResponse](us, req)
94}
95
96func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*types.RepoLogResponse, error) {
97 const (
98 Method = "GET"
99 )
100
101 endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
102
103 query := url.Values{}
104 query.Add("page", strconv.Itoa(page))
105 query.Add("per_page", strconv.Itoa(60))
106
107 req, err := us.newRequest(Method, endpoint, query, nil)
108 if err != nil {
109 return nil, err
110 }
111
112 return do[types.RepoLogResponse](us, req)
113}
114
115func (us *UnsignedClient) Branches(ownerDid, repoName string) (*types.RepoBranchesResponse, error) {
116 const (
117 Method = "GET"
118 )
119
120 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
121
122 req, err := us.newRequest(Method, endpoint, nil, nil)
123 if err != nil {
124 return nil, err
125 }
126
127 return do[types.RepoBranchesResponse](us, req)
128}
129
130func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
131 const (
132 Method = "GET"
133 )
134
135 endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
136
137 req, err := us.newRequest(Method, endpoint, nil, nil)
138 if err != nil {
139 return nil, err
140 }
141
142 resp, err := us.client.Do(req)
143 if err != nil {
144 return nil, err
145 }
146
147 body, err := io.ReadAll(resp.Body)
148 if err != nil {
149 return nil, err
150 }
151
152 var result types.RepoTagsResponse
153 err = json.Unmarshal(body, &result)
154 if err != nil {
155 return nil, err
156 }
157
158 return &result, nil
159}
160
161func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
162 const (
163 Method = "GET"
164 )
165
166 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
167
168 req, err := us.newRequest(Method, endpoint, nil, nil)
169 if err != nil {
170 return nil, err
171 }
172
173 return us.client.Do(req)
174}
175
176func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
177 const (
178 Method = "GET"
179 )
180
181 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
182
183 req, err := us.newRequest(Method, endpoint, nil, nil)
184 if err != nil {
185 return nil, err
186 }
187
188 resp, err := us.client.Do(req)
189 if err != nil {
190 return nil, err
191 }
192 defer resp.Body.Close()
193
194 var defaultBranch types.RepoDefaultBranchResponse
195 if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
196 return nil, err
197 }
198
199 return &defaultBranch, nil
200}
201
202func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
203 const (
204 Method = "GET"
205 Endpoint = "/capabilities"
206 )
207
208 req, err := us.newRequest(Method, Endpoint, nil, nil)
209 if err != nil {
210 return nil, err
211 }
212
213 resp, err := us.client.Do(req)
214 if err != nil {
215 return nil, err
216 }
217 defer resp.Body.Close()
218
219 var capabilities types.Capabilities
220 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
221 return nil, err
222 }
223
224 return &capabilities, nil
225}
226
227func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
228 const (
229 Method = "GET"
230 )
231
232 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
233
234 req, err := us.newRequest(Method, endpoint, nil, nil)
235 if err != nil {
236 return nil, fmt.Errorf("Failed to create request.")
237 }
238
239 compareResp, err := us.client.Do(req)
240 if err != nil {
241 return nil, fmt.Errorf("Failed to create request.")
242 }
243 defer compareResp.Body.Close()
244
245 switch compareResp.StatusCode {
246 case 404:
247 case 400:
248 return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
249 }
250
251 respBody, err := io.ReadAll(compareResp.Body)
252 if err != nil {
253 log.Println("failed to compare across branches")
254 return nil, fmt.Errorf("Failed to compare branches.")
255 }
256 defer compareResp.Body.Close()
257
258 var formatPatchResponse types.RepoFormatPatchResponse
259 err = json.Unmarshal(respBody, &formatPatchResponse)
260 if err != nil {
261 log.Println("failed to unmarshal format-patch response", err)
262 return nil, fmt.Errorf("failed to compare branches.")
263 }
264
265 return &formatPatchResponse, nil
266}