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