A community based topic aggregation platform built on atproto
1package community 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/communities" 6 "encoding/json" 7 "log" 8 "net/http" 9 "regexp" 10 "strings" 11) 12 13// Package-level compiled regex for DID validation (compiled once at startup) 14var ( 15 didRegex = regexp.MustCompile(`^did:(plc|web):[a-zA-Z0-9._:%-]+$`) 16) 17 18// BlockHandler handles community blocking operations 19type BlockHandler struct { 20 service communities.Service 21} 22 23// NewBlockHandler creates a new block handler 24func NewBlockHandler(service communities.Service) *BlockHandler { 25 return &BlockHandler{ 26 service: service, 27 } 28} 29 30// HandleBlock blocks a community 31// POST /xrpc/social.coves.community.blockCommunity 32// 33// Request body: { "community": "did:plc:xxx" } 34// Note: Per lexicon spec, only DIDs are accepted (not handles). 35// The block record's "subject" field requires format: "did". 36func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) { 37 if r.Method != http.MethodPost { 38 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 39 return 40 } 41 42 // Parse request body 43 var req struct { 44 Community string `json:"community"` // DID only (per lexicon) 45 } 46 47 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 48 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 49 return 50 } 51 52 if req.Community == "" { 53 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 54 return 55 } 56 57 // Validate DID format (per lexicon: format must be "did") 58 if !strings.HasPrefix(req.Community, "did:") { 59 writeError(w, http.StatusBadRequest, "InvalidRequest", 60 "community must be a DID (did:plc:... or did:web:...)") 61 return 62 } 63 64 // Validate DID format with regex: did:method:identifier 65 if !didRegex.MatchString(req.Community) { 66 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format") 67 return 68 } 69 70 // Extract authenticated user DID and access token from request context (injected by auth middleware) 71 userDID := middleware.GetUserDID(r) 72 if userDID == "" { 73 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 74 return 75 } 76 77 userAccessToken := middleware.GetUserAccessToken(r) 78 if userAccessToken == "" { 79 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 80 return 81 } 82 83 // Block via service (write-forward to PDS) 84 block, err := h.service.BlockCommunity(r.Context(), userDID, userAccessToken, req.Community) 85 if err != nil { 86 handleServiceError(w, err) 87 return 88 } 89 90 // Return success response (following atProto conventions for block responses) 91 response := map[string]interface{}{ 92 "block": map[string]interface{}{ 93 "recordUri": block.RecordURI, 94 "recordCid": block.RecordCID, 95 }, 96 } 97 98 w.Header().Set("Content-Type", "application/json") 99 w.WriteHeader(http.StatusOK) 100 if err := json.NewEncoder(w).Encode(response); err != nil { 101 log.Printf("Failed to encode response: %v", err) 102 } 103} 104 105// HandleUnblock unblocks a community 106// POST /xrpc/social.coves.community.unblockCommunity 107// 108// Request body: { "community": "did:plc:xxx" } 109// Note: Per lexicon spec, only DIDs are accepted (not handles). 110func (h *BlockHandler) HandleUnblock(w http.ResponseWriter, r *http.Request) { 111 if r.Method != http.MethodPost { 112 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 113 return 114 } 115 116 // Parse request body 117 var req struct { 118 Community string `json:"community"` // DID only (per lexicon) 119 } 120 121 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 122 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 123 return 124 } 125 126 if req.Community == "" { 127 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 128 return 129 } 130 131 // Validate DID format (per lexicon: format must be "did") 132 if !strings.HasPrefix(req.Community, "did:") { 133 writeError(w, http.StatusBadRequest, "InvalidRequest", 134 "community must be a DID (did:plc:... or did:web:...)") 135 return 136 } 137 138 // Validate DID format with regex: did:method:identifier 139 if !didRegex.MatchString(req.Community) { 140 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format") 141 return 142 } 143 144 // Extract authenticated user DID and access token from request context (injected by auth middleware) 145 userDID := middleware.GetUserDID(r) 146 if userDID == "" { 147 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 148 return 149 } 150 151 userAccessToken := middleware.GetUserAccessToken(r) 152 if userAccessToken == "" { 153 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 154 return 155 } 156 157 // Unblock via service (delete record on PDS) 158 err := h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, req.Community) 159 if err != nil { 160 handleServiceError(w, err) 161 return 162 } 163 164 // Return success response 165 w.Header().Set("Content-Type", "application/json") 166 w.WriteHeader(http.StatusOK) 167 if err := json.NewEncoder(w).Encode(map[string]interface{}{ 168 "success": true, 169 }); err != nil { 170 log.Printf("Failed to encode response: %v", err) 171 } 172}