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