A community based topic aggregation platform built on atproto
1package aggregator
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "strconv"
8
9 "Coves/internal/core/aggregators"
10)
11
12// GetAuthorizationsHandler handles listing authorizations for an aggregator
13type GetAuthorizationsHandler struct {
14 service aggregators.Service
15}
16
17// NewGetAuthorizationsHandler creates a new get authorizations handler
18func NewGetAuthorizationsHandler(service aggregators.Service) *GetAuthorizationsHandler {
19 return &GetAuthorizationsHandler{
20 service: service,
21 }
22}
23
24// HandleGetAuthorizations lists all communities that authorized an aggregator
25// GET /xrpc/social.coves.aggregator.getAuthorizations?aggregatorDid=did:plc:abc123&enabledOnly=true&limit=50&cursor=xyz
26// Following Bluesky's pattern for listing feed subscribers
27func (h *GetAuthorizationsHandler) HandleGetAuthorizations(w http.ResponseWriter, r *http.Request) {
28 if r.Method != http.MethodGet {
29 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
30 return
31 }
32
33 // Parse request
34 req, err := h.parseRequest(r)
35 if err != nil {
36 writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error())
37 return
38 }
39
40 // Get aggregator details first (needed for nested aggregator object in response)
41 agg, err := h.service.GetAggregator(r.Context(), req.AggregatorDID)
42 if err != nil {
43 if aggregators.IsNotFound(err) {
44 writeError(w, http.StatusNotFound, "AggregatorNotFound", "Aggregator DID does not exist or has no service declaration")
45 return
46 }
47 handleServiceError(w, err)
48 return
49 }
50
51 // Get authorizations from service
52 auths, err := h.service.GetAuthorizationsForAggregator(r.Context(), req)
53 if err != nil {
54 handleServiceError(w, err)
55 return
56 }
57
58 // Build response
59 response := GetAuthorizationsResponse{
60 Authorizations: make([]CommunityAuthView, 0, len(auths)),
61 }
62
63 // Convert aggregator to view for nesting in each authorization
64 aggregatorView := toAggregatorView(agg)
65
66 for _, auth := range auths {
67 response.Authorizations = append(response.Authorizations, toCommunityAuthView(auth, aggregatorView))
68 }
69
70 // Return response
71 w.Header().Set("Content-Type", "application/json")
72 w.WriteHeader(http.StatusOK)
73 if err := json.NewEncoder(w).Encode(response); err != nil {
74 log.Printf("ERROR: Failed to encode getAuthorizations response: %v", err)
75 }
76}
77
78// parseRequest parses query parameters
79func (h *GetAuthorizationsHandler) parseRequest(r *http.Request) (aggregators.GetAuthorizationsRequest, error) {
80 req := aggregators.GetAuthorizationsRequest{}
81
82 // Required: aggregatorDid
83 req.AggregatorDID = r.URL.Query().Get("aggregatorDid")
84
85 // Optional: enabledOnly (default: false)
86 if enabledOnlyStr := r.URL.Query().Get("enabledOnly"); enabledOnlyStr == "true" {
87 req.EnabledOnly = true
88 }
89
90 // Optional: limit (default: 50, set by service)
91 if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
92 if limit, err := strconv.Atoi(limitStr); err == nil {
93 req.Limit = limit
94 }
95 }
96
97 // Optional: offset (default: 0)
98 if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
99 if offset, err := strconv.Atoi(offsetStr); err == nil {
100 req.Offset = offset
101 }
102 }
103
104 return req, nil
105}
106
107// GetAuthorizationsResponse matches the lexicon output
108type GetAuthorizationsResponse struct {
109 Cursor *string `json:"cursor,omitempty"`
110 Authorizations []CommunityAuthView `json:"authorizations"`
111}
112
113// CommunityAuthView matches social.coves.aggregator.defs#communityAuthView
114// Shows authorization from aggregator's perspective with nested aggregator details
115type CommunityAuthView struct {
116 Config interface{} `json:"config,omitempty"`
117 Aggregator AggregatorView `json:"aggregator"`
118 CreatedAt string `json:"createdAt"`
119 RecordUri string `json:"recordUri,omitempty"`
120 Enabled bool `json:"enabled"`
121}
122
123// toCommunityAuthView converts domain model to API view
124func toCommunityAuthView(auth *aggregators.Authorization, aggregatorView AggregatorView) CommunityAuthView {
125 view := CommunityAuthView{
126 Aggregator: aggregatorView, // Nested aggregator object
127 Enabled: auth.Enabled,
128 CreatedAt: auth.CreatedAt.Format("2006-01-02T15:04:05.000Z"),
129 }
130
131 // Add optional fields
132 if len(auth.Config) > 0 {
133 // Config is JSONB, unmarshal it
134 var config interface{}
135 if err := json.Unmarshal(auth.Config, &config); err == nil {
136 view.Config = config
137 }
138 }
139 if auth.RecordURI != "" {
140 view.RecordUri = auth.RecordURI
141 }
142
143 return view
144}