A community based topic aggregation platform built on atproto
1package community 2 3import ( 4 "encoding/json" 5 "log" 6 "net/http" 7 "strings" 8 9 "Coves/internal/api/middleware" 10 "Coves/internal/core/communities" 11) 12 13// SubscribeHandler handles community subscriptions 14type SubscribeHandler struct { 15 service communities.Service 16} 17 18// NewSubscribeHandler creates a new subscribe handler 19func NewSubscribeHandler(service communities.Service) *SubscribeHandler { 20 return &SubscribeHandler{ 21 service: service, 22 } 23} 24 25// HandleSubscribe subscribes a user to a community 26// POST /xrpc/social.coves.community.subscribe 27// 28// Request body: { "community": "did:plc:xxx", "contentVisibility": 3 } 29// Note: Per lexicon spec, only DIDs are accepted for the "subject" field (not handles). 30func (h *SubscribeHandler) HandleSubscribe(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"` // DID only (per lexicon) 39 ContentVisibility int `json:"contentVisibility"` // Optional: 1-5 scale, defaults to 3 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: subject field requires format "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 // Extract authenticated user DID and access token from request context (injected by auth middleware) 60 // Note: contentVisibility defaults and clamping handled by service layer 61 userDID := middleware.GetUserDID(r) 62 if userDID == "" { 63 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 64 return 65 } 66 67 userAccessToken := middleware.GetUserAccessToken(r) 68 if userAccessToken == "" { 69 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 70 return 71 } 72 73 // Subscribe via service (write-forward to PDS) 74 subscription, err := h.service.SubscribeToCommunity(r.Context(), userDID, userAccessToken, req.Community, req.ContentVisibility) 75 if err != nil { 76 handleServiceError(w, err) 77 return 78 } 79 80 // Return success response 81 response := map[string]interface{}{ 82 "uri": subscription.RecordURI, 83 "cid": subscription.RecordCID, 84 "existing": false, // Would be true if already subscribed 85 } 86 87 w.Header().Set("Content-Type", "application/json") 88 w.WriteHeader(http.StatusOK) 89 if err := json.NewEncoder(w).Encode(response); err != nil { 90 log.Printf("Failed to encode response: %v", err) 91 } 92} 93 94// HandleUnsubscribe unsubscribes a user from a community 95// POST /xrpc/social.coves.community.unsubscribe 96// 97// Request body: { "community": "did:plc:xxx" } 98// Note: Per lexicon spec, only DIDs are accepted (not handles). 99func (h *SubscribeHandler) HandleUnsubscribe(w http.ResponseWriter, r *http.Request) { 100 if r.Method != http.MethodPost { 101 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 102 return 103 } 104 105 // Parse request body 106 var req struct { 107 Community string `json:"community"` // DID only (per lexicon) 108 } 109 110 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 111 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 112 return 113 } 114 115 if req.Community == "" { 116 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 117 return 118 } 119 120 // Validate DID format (per lexicon: subject field requires format "did") 121 if !strings.HasPrefix(req.Community, "did:") { 122 writeError(w, http.StatusBadRequest, "InvalidRequest", 123 "community must be a DID (did:plc:... or did:web:...)") 124 return 125 } 126 127 // Extract authenticated user DID and access token from request context (injected by auth middleware) 128 userDID := middleware.GetUserDID(r) 129 if userDID == "" { 130 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 131 return 132 } 133 134 userAccessToken := middleware.GetUserAccessToken(r) 135 if userAccessToken == "" { 136 writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 137 return 138 } 139 140 // Unsubscribe via service (delete record on PDS) 141 err := h.service.UnsubscribeFromCommunity(r.Context(), userDID, userAccessToken, req.Community) 142 if err != nil { 143 handleServiceError(w, err) 144 return 145 } 146 147 // Return success response 148 w.Header().Set("Content-Type", "application/json") 149 w.WriteHeader(http.StatusOK) 150 if err := json.NewEncoder(w).Encode(map[string]interface{}{ 151 "success": true, 152 }); err != nil { 153 log.Printf("Failed to encode response: %v", err) 154 } 155}