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// Package-level compiled regex for DID validation (compiled once at startup)
14var (
15 didRegex = regexp.MustCompile(`^did:(plc|web):[a-zA-Z0-9._:%-]+$`)
16)
17
18// BlockHandler handles community blocking operations
19type BlockHandler struct {
20 service communities.Service
21}
22
23// NewBlockHandler creates a new block handler
24func NewBlockHandler(service communities.Service) *BlockHandler {
25 return &BlockHandler{
26 service: service,
27 }
28}
29
30// HandleBlock blocks a community
31// POST /xrpc/social.coves.community.blockCommunity
32//
33// Request body: { "community": "did:plc:xxx" }
34// Note: Per lexicon spec, only DIDs are accepted (not handles).
35// The block record's "subject" field requires format: "did".
36func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) {
37 if r.Method != http.MethodPost {
38 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
39 return
40 }
41
42 // Parse request body
43 var req struct {
44 Community string `json:"community"` // DID only (per lexicon)
45 }
46
47 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
48 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body")
49 return
50 }
51
52 if req.Community == "" {
53 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required")
54 return
55 }
56
57 // Validate DID format (per lexicon: format must be "did")
58 if !strings.HasPrefix(req.Community, "did:") {
59 writeError(w, http.StatusBadRequest, "InvalidRequest",
60 "community must be a DID (did:plc:... or did:web:...)")
61 return
62 }
63
64 // Validate DID format with regex: did:method:identifier
65 if !didRegex.MatchString(req.Community) {
66 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format")
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.unblockCommunity
107//
108// Request body: { "community": "did:plc:xxx" }
109// Note: Per lexicon spec, only DIDs are accepted (not handles).
110func (h *BlockHandler) HandleUnblock(w http.ResponseWriter, r *http.Request) {
111 if r.Method != http.MethodPost {
112 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
113 return
114 }
115
116 // Parse request body
117 var req struct {
118 Community string `json:"community"` // DID only (per lexicon)
119 }
120
121 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
122 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body")
123 return
124 }
125
126 if req.Community == "" {
127 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required")
128 return
129 }
130
131 // Validate DID format (per lexicon: format must be "did")
132 if !strings.HasPrefix(req.Community, "did:") {
133 writeError(w, http.StatusBadRequest, "InvalidRequest",
134 "community must be a DID (did:plc:... or did:web:...)")
135 return
136 }
137
138 // Validate DID format with regex: did:method:identifier
139 if !didRegex.MatchString(req.Community) {
140 writeError(w, http.StatusBadRequest, "InvalidRequest", "invalid DID format")
141 return
142 }
143
144 // Extract authenticated user DID and access token from request context (injected by auth middleware)
145 userDID := middleware.GetUserDID(r)
146 if userDID == "" {
147 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required")
148 return
149 }
150
151 userAccessToken := middleware.GetUserAccessToken(r)
152 if userAccessToken == "" {
153 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token")
154 return
155 }
156
157 // Unblock via service (delete record on PDS)
158 err := h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, req.Community)
159 if err != nil {
160 handleServiceError(w, err)
161 return
162 }
163
164 // Return success response
165 w.Header().Set("Content-Type", "application/json")
166 w.WriteHeader(http.StatusOK)
167 if err := json.NewEncoder(w).Encode(map[string]interface{}{
168 "success": true,
169 }); err != nil {
170 log.Printf("Failed to encode response: %v", err)
171 }
172}