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}