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 "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}