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}