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}