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