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