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