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}