A community based topic aggregation platform built on atproto
1package community 2 3import ( 4 "encoding/json" 5 "log" 6 "net/http" 7 8 "Coves/internal/api/middleware" 9 "Coves/internal/core/communities" 10) 11 12// BlockHandler handles community blocking operations 13type BlockHandler struct { 14 service communities.Service 15} 16 17// NewBlockHandler creates a new block handler 18func NewBlockHandler(service communities.Service) *BlockHandler { 19 return &BlockHandler{ 20 service: service, 21 } 22} 23 24// HandleBlock blocks a community 25// POST /xrpc/social.coves.community.blockCommunity 26// 27// Request body: { "community": "at-identifier" } 28// Accepts DIDs (did:plc:xxx), handles (@gaming.community.coves.social), or scoped (!gaming@coves.social) 29// The block record's "subject" field requires format: "did", so we resolve the identifier internally. 30func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) { 31 if r.Method != http.MethodPost { 32 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 33 return 34 } 35 36 // Parse request body 37 var req struct { 38 Community string `json:"community"` // at-identifier (DID or handle) 39 } 40 41 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 42 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 43 return 44 } 45 46 if req.Community == "" { 47 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 48 return 49 } 50 51 // Extract authenticated user DID and access token from request context (injected by auth middleware) 52 userDID := middleware.GetUserDID(r) 53 if userDID == "" { 54 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 55 return 56 } 57 58 userAccessToken := middleware.GetUserAccessToken(r) 59 if userAccessToken == "" { 60 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 61 return 62 } 63 64 // Resolve community identifier (handle or DID) to DID 65 // This allows users to block by handle: @gaming.community.coves.social or !gaming@coves.social 66 communityDID, err := h.service.ResolveCommunityIdentifier(r.Context(), req.Community) 67 if err != nil { 68 if communities.IsNotFound(err) { 69 writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found") 70 return 71 } 72 if communities.IsValidationError(err) { 73 writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 74 return 75 } 76 log.Printf("Failed to resolve community identifier %s: %v", req.Community, err) 77 writeError(w, http.StatusInternalServerError, "InternalError", "Failed to resolve community") 78 return 79 } 80 81 // Block via service (write-forward to PDS) using resolved DID 82 block, err := h.service.BlockCommunity(r.Context(), userDID, userAccessToken, communityDID) 83 if err != nil { 84 handleServiceError(w, err) 85 return 86 } 87 88 // Return success response (following atProto conventions for block responses) 89 response := map[string]interface{}{ 90 "block": map[string]interface{}{ 91 "recordUri": block.RecordURI, 92 "recordCid": block.RecordCID, 93 }, 94 } 95 96 w.Header().Set("Content-Type", "application/json") 97 w.WriteHeader(http.StatusOK) 98 if err := json.NewEncoder(w).Encode(response); err != nil { 99 log.Printf("Failed to encode response: %v", err) 100 } 101} 102 103// HandleUnblock unblocks a community 104// POST /xrpc/social.coves.community.unblockCommunity 105// 106// Request body: { "community": "at-identifier" } 107// Accepts DIDs (did:plc:xxx), handles (@gaming.community.coves.social), or scoped (!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"` // at-identifier (DID or handle) 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 // Extract authenticated user DID and access token from request context (injected by auth middleware) 130 userDID := middleware.GetUserDID(r) 131 if userDID == "" { 132 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 133 return 134 } 135 136 userAccessToken := middleware.GetUserAccessToken(r) 137 if userAccessToken == "" { 138 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 139 return 140 } 141 142 // Resolve community identifier (handle or DID) to DID 143 // This allows users to unblock by handle: @gaming.community.coves.social or !gaming@coves.social 144 communityDID, err := h.service.ResolveCommunityIdentifier(r.Context(), req.Community) 145 if err != nil { 146 if communities.IsNotFound(err) { 147 writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found") 148 return 149 } 150 if communities.IsValidationError(err) { 151 writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 152 return 153 } 154 log.Printf("Failed to resolve community identifier %s: %v", req.Community, err) 155 writeError(w, http.StatusInternalServerError, "InternalError", "Failed to resolve community") 156 return 157 } 158 159 // Unblock via service (delete record on PDS) using resolved DID 160 err = h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, communityDID) 161 if err != nil { 162 handleServiceError(w, err) 163 return 164 } 165 166 // Return success response 167 w.Header().Set("Content-Type", "application/json") 168 w.WriteHeader(http.StatusOK) 169 if err := json.NewEncoder(w).Encode(map[string]interface{}{ 170 "success": true, 171 }); err != nil { 172 log.Printf("Failed to encode response: %v", err) 173 } 174}