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.block 27// Body: { "community": "did:plc:xxx" or "!gaming@coves.social" } 28func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) { 29 if r.Method != http.MethodPost { 30 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 31 return 32 } 33 34 // Parse request body 35 var req struct { 36 Community string `json:"community"` // DID or handle 37 } 38 39 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 40 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 41 return 42 } 43 44 if req.Community == "" { 45 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 46 return 47 } 48 49 // Validate format (DID or handle) with proper regex patterns 50 if strings.HasPrefix(req.Community, "did:") { 51 // Validate DID format: did:method:identifier 52 // atProto supports did:plc and did:web 53 didRegex := regexp.MustCompile(`^did:(plc|web):[a-zA-Z0-9._:%-]+$`) 54 if !didRegex.MatchString(req.Community) { 55 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format") 56 return 57 } 58 } else if strings.HasPrefix(req.Community, "!") { 59 // Validate handle format: !name@domain.tld 60 if !strings.Contains(req.Community, "@") { 61 writeError(w, http.StatusBadRequest, "InvalidRequest", "handle must contain @domain") 62 return 63 } 64 } else { 65 writeError(w, http.StatusBadRequest, "InvalidRequest", 66 "community must be a DID (did:plc:...) or handle (!name@instance.com)") 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.unblock 107// Body: { "community": "did:plc:xxx" or "!gaming@coves.social" } 108func (h *BlockHandler) HandleUnblock(w http.ResponseWriter, r *http.Request) { 109 if r.Method != http.MethodPost { 110 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 111 return 112 } 113 114 // Parse request body 115 var req struct { 116 Community string `json:"community"` 117 } 118 119 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 120 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 121 return 122 } 123 124 if req.Community == "" { 125 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 126 return 127 } 128 129 // Validate format (DID or handle) with proper regex patterns 130 if strings.HasPrefix(req.Community, "did:") { 131 // Validate DID format: did:method:identifier 132 didRegex := regexp.MustCompile(`^did:(plc|web):[a-zA-Z0-9._:%-]+$`) 133 if !didRegex.MatchString(req.Community) { 134 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format") 135 return 136 } 137 } else if strings.HasPrefix(req.Community, "!") { 138 // Validate handle format: !name@domain.tld 139 if !strings.Contains(req.Community, "@") { 140 writeError(w, http.StatusBadRequest, "InvalidRequest", "handle must contain @domain") 141 return 142 } 143 } else { 144 writeError(w, http.StatusBadRequest, "InvalidRequest", 145 "community must be a DID (did:plc:...) or handle (!name@instance.com)") 146 return 147 } 148 149 // Extract authenticated user DID and access token from request context (injected by auth middleware) 150 userDID := middleware.GetUserDID(r) 151 if userDID == "" { 152 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 153 return 154 } 155 156 userAccessToken := middleware.GetUserAccessToken(r) 157 if userAccessToken == "" { 158 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 159 return 160 } 161 162 // Unblock via service (delete record on PDS) 163 err := h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, req.Community) 164 if err != nil { 165 handleServiceError(w, err) 166 return 167 } 168 169 // Return success response 170 w.Header().Set("Content-Type", "application/json") 171 w.WriteHeader(http.StatusOK) 172 if err := json.NewEncoder(w).Encode(map[string]interface{}{ 173 "success": true, 174 }); err != nil { 175 log.Printf("Failed to encode response: %v", err) 176 } 177}