A community based topic aggregation platform built on atproto
1package community
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "regexp"
8 "strings"
9
10 "Coves/internal/api/middleware"
11 "Coves/internal/core/communities"
12)
13
14// Package-level compiled regex for DID validation (compiled once at startup)
15var (
16 didRegex = regexp.MustCompile(`^did:(plc|web):[a-zA-Z0-9._:%-]+$`)
17)
18
19// BlockHandler handles community blocking operations
20type BlockHandler struct {
21 service communities.Service
22}
23
24// NewBlockHandler creates a new block handler
25func NewBlockHandler(service communities.Service) *BlockHandler {
26 return &BlockHandler{
27 service: service,
28 }
29}
30
31// HandleBlock blocks a community
32// POST /xrpc/social.coves.community.blockCommunity
33//
34// Request body: { "community": "did:plc:xxx" }
35// Note: Per lexicon spec, only DIDs are accepted (not handles).
36// The block record's "subject" field requires format: "did".
37func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) {
38 if r.Method != http.MethodPost {
39 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
40 return
41 }
42
43 // Parse request body
44 var req struct {
45 Community string `json:"community"` // DID only (per lexicon)
46 }
47
48 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
49 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body")
50 return
51 }
52
53 if req.Community == "" {
54 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required")
55 return
56 }
57
58 // Validate DID format (per lexicon: format must be "did")
59 if !strings.HasPrefix(req.Community, "did:") {
60 writeError(w, http.StatusBadRequest, "InvalidRequest",
61 "community must be a DID (did:plc:... or did:web:...)")
62 return
63 }
64
65 // Validate DID format with regex: did:method:identifier
66 if !didRegex.MatchString(req.Community) {
67 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format")
68 return
69 }
70
71 // Extract authenticated user DID and access token from request context (injected by auth middleware)
72 userDID := middleware.GetUserDID(r)
73 if userDID == "" {
74 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required")
75 return
76 }
77
78 userAccessToken := middleware.GetUserAccessToken(r)
79 if userAccessToken == "" {
80 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token")
81 return
82 }
83
84 // Block via service (write-forward to PDS)
85 block, err := h.service.BlockCommunity(r.Context(), userDID, userAccessToken, req.Community)
86 if err != nil {
87 handleServiceError(w, err)
88 return
89 }
90
91 // Return success response (following atProto conventions for block responses)
92 response := map[string]interface{}{
93 "block": map[string]interface{}{
94 "recordUri": block.RecordURI,
95 "recordCid": block.RecordCID,
96 },
97 }
98
99 w.Header().Set("Content-Type", "application/json")
100 w.WriteHeader(http.StatusOK)
101 if err := json.NewEncoder(w).Encode(response); err != nil {
102 log.Printf("Failed to encode response: %v", err)
103 }
104}
105
106// HandleUnblock unblocks a community
107// POST /xrpc/social.coves.community.unblockCommunity
108//
109// Request body: { "community": "did:plc:xxx" }
110// Note: Per lexicon spec, only DIDs are accepted (not handles).
111func (h *BlockHandler) HandleUnblock(w http.ResponseWriter, r *http.Request) {
112 if r.Method != http.MethodPost {
113 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
114 return
115 }
116
117 // Parse request body
118 var req struct {
119 Community string `json:"community"` // DID only (per lexicon)
120 }
121
122 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
123 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body")
124 return
125 }
126
127 if req.Community == "" {
128 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required")
129 return
130 }
131
132 // Validate DID format (per lexicon: format must be "did")
133 if !strings.HasPrefix(req.Community, "did:") {
134 writeError(w, http.StatusBadRequest, "InvalidRequest",
135 "community must be a DID (did:plc:... or did:web:...)")
136 return
137 }
138
139 // Validate DID format with regex: did:method:identifier
140 if !didRegex.MatchString(req.Community) {
141 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format")
142 return
143 }
144
145 // Extract authenticated user DID and access token from request context (injected by auth middleware)
146 userDID := middleware.GetUserDID(r)
147 if userDID == "" {
148 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required")
149 return
150 }
151
152 userAccessToken := middleware.GetUserAccessToken(r)
153 if userAccessToken == "" {
154 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token")
155 return
156 }
157
158 // Unblock via service (delete record on PDS)
159 err := h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, req.Community)
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}