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}