A community based topic aggregation platform built on atproto
1package handlers
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "strings"
9
10 "Coves/internal/core/repository"
11 "github.com/ipfs/go-cid"
12 cbornode "github.com/ipfs/go-ipld-cbor"
13)
14
15// RepositoryHandler handles HTTP requests for repository operations
16type RepositoryHandler struct {
17 service repository.RepositoryService
18}
19
20// NewRepositoryHandler creates a new repository handler
21func NewRepositoryHandler(service repository.RepositoryService) *RepositoryHandler {
22 return &RepositoryHandler{
23 service: service,
24 }
25}
26
27// AT Protocol XRPC request/response types
28
29// CreateRecordRequest represents a request to create a record
30type CreateRecordRequest struct {
31 Repo string `json:"repo"` // DID of the repository
32 Collection string `json:"collection"` // NSID of the collection
33 RKey string `json:"rkey,omitempty"` // Optional record key
34 Validate bool `json:"validate"` // Whether to validate against lexicon
35 Record json.RawMessage `json:"record"` // The record data
36}
37
38// CreateRecordResponse represents the response after creating a record
39type CreateRecordResponse struct {
40 URI string `json:"uri"` // AT-URI of the created record
41 CID string `json:"cid"` // CID of the record
42}
43
44// GetRecordRequest represents a request to get a record
45type GetRecordRequest struct {
46 Repo string `json:"repo"` // DID of the repository
47 Collection string `json:"collection"` // NSID of the collection
48 RKey string `json:"rkey"` // Record key
49}
50
51// GetRecordResponse represents the response when getting a record
52type GetRecordResponse struct {
53 URI string `json:"uri"` // AT-URI of the record
54 CID string `json:"cid"` // CID of the record
55 Value json.RawMessage `json:"value"` // The record data
56}
57
58// PutRecordRequest represents a request to update a record
59type PutRecordRequest struct {
60 Repo string `json:"repo"` // DID of the repository
61 Collection string `json:"collection"` // NSID of the collection
62 RKey string `json:"rkey"` // Record key
63 Validate bool `json:"validate"` // Whether to validate against lexicon
64 Record json.RawMessage `json:"record"` // The record data
65}
66
67// PutRecordResponse represents the response after updating a record
68type PutRecordResponse struct {
69 URI string `json:"uri"` // AT-URI of the updated record
70 CID string `json:"cid"` // CID of the record
71}
72
73// DeleteRecordRequest represents a request to delete a record
74type DeleteRecordRequest struct {
75 Repo string `json:"repo"` // DID of the repository
76 Collection string `json:"collection"` // NSID of the collection
77 RKey string `json:"rkey"` // Record key
78}
79
80// ListRecordsRequest represents a request to list records
81type ListRecordsRequest struct {
82 Repo string `json:"repo"` // DID of the repository
83 Collection string `json:"collection"` // NSID of the collection
84 Limit int `json:"limit,omitempty"`
85 Cursor string `json:"cursor,omitempty"`
86}
87
88// ListRecordsResponse represents the response when listing records
89type ListRecordsResponse struct {
90 Cursor string `json:"cursor,omitempty"`
91 Records []RecordOutput `json:"records"`
92}
93
94// RecordOutput represents a record in list responses
95type RecordOutput struct {
96 URI string `json:"uri"`
97 CID string `json:"cid"`
98 Value json.RawMessage `json:"value"`
99}
100
101// Handler methods
102
103// CreateRecord handles POST /xrpc/com.atproto.repo.createRecord
104func (h *RepositoryHandler) CreateRecord(w http.ResponseWriter, r *http.Request) {
105 var req CreateRecordRequest
106 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
107 writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
108 return
109 }
110
111 // Validate required fields
112 if req.Repo == "" || req.Collection == "" || len(req.Record) == 0 {
113 writeError(w, http.StatusBadRequest, "missing required fields")
114 return
115 }
116
117 // Create a generic record structure for CBOR encoding
118 // In a real implementation, you would unmarshal to the specific lexicon type
119 recordData := &GenericRecord{
120 Data: req.Record,
121 }
122
123 input := repository.CreateRecordInput{
124 DID: req.Repo,
125 Collection: req.Collection,
126 RecordKey: req.RKey,
127 Record: recordData,
128 Validate: req.Validate,
129 }
130
131 record, err := h.service.CreateRecord(input)
132 if err != nil {
133 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create record: %v", err))
134 return
135 }
136
137 resp := CreateRecordResponse{
138 URI: record.URI,
139 CID: record.CID.String(),
140 }
141
142 writeJSON(w, http.StatusOK, resp)
143}
144
145// GetRecord handles GET /xrpc/com.atproto.repo.getRecord
146func (h *RepositoryHandler) GetRecord(w http.ResponseWriter, r *http.Request) {
147 // Parse query parameters
148 repo := r.URL.Query().Get("repo")
149 collection := r.URL.Query().Get("collection")
150 rkey := r.URL.Query().Get("rkey")
151
152 if repo == "" || collection == "" || rkey == "" {
153 writeError(w, http.StatusBadRequest, "missing required parameters")
154 return
155 }
156
157 input := repository.GetRecordInput{
158 DID: repo,
159 Collection: collection,
160 RecordKey: rkey,
161 }
162
163 record, err := h.service.GetRecord(input)
164 if err != nil {
165 if strings.Contains(err.Error(), "not found") {
166 writeError(w, http.StatusNotFound, "record not found")
167 return
168 }
169 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get record: %v", err))
170 return
171 }
172
173 resp := GetRecordResponse{
174 URI: record.URI,
175 CID: record.CID.String(),
176 Value: json.RawMessage(record.Value),
177 }
178
179 writeJSON(w, http.StatusOK, resp)
180}
181
182// PutRecord handles POST /xrpc/com.atproto.repo.putRecord
183func (h *RepositoryHandler) PutRecord(w http.ResponseWriter, r *http.Request) {
184 var req PutRecordRequest
185 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
186 writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
187 return
188 }
189
190 // Validate required fields
191 if req.Repo == "" || req.Collection == "" || req.RKey == "" || len(req.Record) == 0 {
192 writeError(w, http.StatusBadRequest, "missing required fields")
193 return
194 }
195
196 // Create a generic record structure for CBOR encoding
197 recordData := &GenericRecord{
198 Data: req.Record,
199 }
200
201 input := repository.UpdateRecordInput{
202 DID: req.Repo,
203 Collection: req.Collection,
204 RecordKey: req.RKey,
205 Record: recordData,
206 Validate: req.Validate,
207 }
208
209 record, err := h.service.UpdateRecord(input)
210 if err != nil {
211 if strings.Contains(err.Error(), "not found") {
212 writeError(w, http.StatusNotFound, "record not found")
213 return
214 }
215 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to update record: %v", err))
216 return
217 }
218
219 resp := PutRecordResponse{
220 URI: record.URI,
221 CID: record.CID.String(),
222 }
223
224 writeJSON(w, http.StatusOK, resp)
225}
226
227// DeleteRecord handles POST /xrpc/com.atproto.repo.deleteRecord
228func (h *RepositoryHandler) DeleteRecord(w http.ResponseWriter, r *http.Request) {
229 var req DeleteRecordRequest
230 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
231 writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
232 return
233 }
234
235 // Validate required fields
236 if req.Repo == "" || req.Collection == "" || req.RKey == "" {
237 writeError(w, http.StatusBadRequest, "missing required fields")
238 return
239 }
240
241 input := repository.DeleteRecordInput{
242 DID: req.Repo,
243 Collection: req.Collection,
244 RecordKey: req.RKey,
245 }
246
247 err := h.service.DeleteRecord(input)
248 if err != nil {
249 if strings.Contains(err.Error(), "not found") {
250 writeError(w, http.StatusNotFound, "record not found")
251 return
252 }
253 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to delete record: %v", err))
254 return
255 }
256
257 w.WriteHeader(http.StatusOK)
258 w.Write([]byte("{}"))
259}
260
261// ListRecords handles GET /xrpc/com.atproto.repo.listRecords
262func (h *RepositoryHandler) ListRecords(w http.ResponseWriter, r *http.Request) {
263 // Parse query parameters
264 repo := r.URL.Query().Get("repo")
265 collection := r.URL.Query().Get("collection")
266 limit := 50 // Default limit
267 cursor := r.URL.Query().Get("cursor")
268
269 if repo == "" || collection == "" {
270 writeError(w, http.StatusBadRequest, "missing required parameters")
271 return
272 }
273
274 // Parse limit if provided
275 if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
276 fmt.Sscanf(limitStr, "%d", &limit)
277 if limit > 100 {
278 limit = 100 // Max limit
279 }
280 }
281
282 records, nextCursor, err := h.service.ListRecords(repo, collection, limit, cursor)
283 if err != nil {
284 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to list records: %v", err))
285 return
286 }
287
288 // Convert to output format
289 recordOutputs := make([]RecordOutput, len(records))
290 for i, record := range records {
291 recordOutputs[i] = RecordOutput{
292 URI: record.URI,
293 CID: record.CID.String(),
294 Value: json.RawMessage(record.Value),
295 }
296 }
297
298 resp := ListRecordsResponse{
299 Cursor: nextCursor,
300 Records: recordOutputs,
301 }
302
303 writeJSON(w, http.StatusOK, resp)
304}
305
306// GetRepo handles GET /xrpc/com.atproto.sync.getRepo
307func (h *RepositoryHandler) GetRepo(w http.ResponseWriter, r *http.Request) {
308 // Parse query parameters
309 did := r.URL.Query().Get("did")
310 if did == "" {
311 writeError(w, http.StatusBadRequest, "missing did parameter")
312 return
313 }
314
315 // Export repository as CAR file
316 carData, err := h.service.ExportRepository(did)
317 if err != nil {
318 if strings.Contains(err.Error(), "not found") {
319 writeError(w, http.StatusNotFound, "repository not found")
320 return
321 }
322 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to export repository: %v", err))
323 return
324 }
325
326 // Set appropriate headers for CAR file
327 w.Header().Set("Content-Type", "application/vnd.ipld.car")
328 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(carData)))
329 w.WriteHeader(http.StatusOK)
330 w.Write(carData)
331}
332
333// Additional repository management endpoints
334
335// CreateRepository handles POST /xrpc/com.atproto.repo.createRepo
336func (h *RepositoryHandler) CreateRepository(w http.ResponseWriter, r *http.Request) {
337 var req struct {
338 DID string `json:"did"`
339 }
340 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
341 writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
342 return
343 }
344
345 if req.DID == "" {
346 writeError(w, http.StatusBadRequest, "missing did")
347 return
348 }
349
350 repo, err := h.service.CreateRepository(req.DID)
351 if err != nil {
352 if strings.Contains(err.Error(), "already exists") {
353 writeError(w, http.StatusConflict, "repository already exists")
354 return
355 }
356 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create repository: %v", err))
357 return
358 }
359
360 resp := struct {
361 DID string `json:"did"`
362 HeadCID string `json:"head"`
363 }{
364 DID: repo.DID,
365 HeadCID: repo.HeadCID.String(),
366 }
367
368 writeJSON(w, http.StatusOK, resp)
369}
370
371// GetCommit handles GET /xrpc/com.atproto.sync.getCommit
372func (h *RepositoryHandler) GetCommit(w http.ResponseWriter, r *http.Request) {
373 // Parse query parameters
374 did := r.URL.Query().Get("did")
375 commitCIDStr := r.URL.Query().Get("cid")
376
377 if did == "" || commitCIDStr == "" {
378 writeError(w, http.StatusBadRequest, "missing required parameters")
379 return
380 }
381
382 // Parse CID
383 commitCID, err := cid.Parse(commitCIDStr)
384 if err != nil {
385 writeError(w, http.StatusBadRequest, "invalid cid")
386 return
387 }
388
389 commit, err := h.service.GetCommit(did, commitCID)
390 if err != nil {
391 if strings.Contains(err.Error(), "not found") {
392 writeError(w, http.StatusNotFound, "commit not found")
393 return
394 }
395 writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get commit: %v", err))
396 return
397 }
398
399 resp := struct {
400 CID string `json:"cid"`
401 DID string `json:"did"`
402 Version int `json:"version"`
403 PrevCID *string `json:"prev,omitempty"`
404 DataCID string `json:"data"`
405 Revision string `json:"rev"`
406 Signature string `json:"sig"`
407 CreatedAt string `json:"createdAt"`
408 }{
409 CID: commit.CID.String(),
410 DID: commit.DID,
411 Version: commit.Version,
412 DataCID: commit.DataCID.String(),
413 Revision: commit.Revision,
414 Signature: fmt.Sprintf("%x", commit.Signature),
415 CreatedAt: commit.CreatedAt.Format("2006-01-02T15:04:05Z"),
416 }
417
418 if commit.PrevCID != nil {
419 prev := commit.PrevCID.String()
420 resp.PrevCID = &prev
421 }
422
423 writeJSON(w, http.StatusOK, resp)
424}
425
426// Helper functions
427
428func writeJSON(w http.ResponseWriter, status int, data interface{}) {
429 w.Header().Set("Content-Type", "application/json")
430 w.WriteHeader(status)
431 json.NewEncoder(w).Encode(data)
432}
433
434func writeError(w http.ResponseWriter, status int, message string) {
435 w.Header().Set("Content-Type", "application/json")
436 w.WriteHeader(status)
437 json.NewEncoder(w).Encode(map[string]interface{}{
438 "error": http.StatusText(status),
439 "message": message,
440 })
441}
442
443// GenericRecord is a temporary structure for CBOR encoding
444// In a real implementation, you would have specific types for each lexicon
445type GenericRecord struct {
446 Data json.RawMessage
447}
448
449// MarshalCBOR implements the CBORMarshaler interface
450func (g *GenericRecord) MarshalCBOR(w io.Writer) error {
451 // Parse JSON data into a generic map for proper CBOR encoding
452 var data map[string]interface{}
453 if err := json.Unmarshal(g.Data, &data); err != nil {
454 return fmt.Errorf("failed to unmarshal JSON data: %w", err)
455 }
456
457 // Use IPFS CBOR encoding to properly encode the data
458 cborData, err := cbornode.DumpObject(data)
459 if err != nil {
460 return fmt.Errorf("failed to marshal as CBOR: %w", err)
461 }
462
463 _, err = w.Write(cborData)
464 if err != nil {
465 return fmt.Errorf("failed to write CBOR data: %w", err)
466 }
467
468 return nil
469}