A community based topic aggregation platform built on atproto
at main 5.9 kB view raw
1package aggregator 2 3import ( 4 "Coves/internal/core/aggregators" 5 "encoding/json" 6 "log" 7 "net/http" 8 "strings" 9) 10 11// GetServicesHandler handles aggregator service details retrieval 12type GetServicesHandler struct { 13 service aggregators.Service 14} 15 16// NewGetServicesHandler creates a new get services handler 17func NewGetServicesHandler(service aggregators.Service) *GetServicesHandler { 18 return &GetServicesHandler{ 19 service: service, 20 } 21} 22 23// HandleGetServices retrieves aggregator details by DID(s) 24// GET /xrpc/social.coves.aggregator.getServices?dids=did:plc:abc123,did:plc:def456&detailed=true 25// Following Bluesky's pattern: app.bsky.feed.getFeedGenerators 26func (h *GetServicesHandler) HandleGetServices(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 DIDs from query parameter 33 didsParam := r.URL.Query().Get("dids") 34 if didsParam == "" { 35 writeError(w, http.StatusBadRequest, "InvalidRequest", "dids parameter is required") 36 return 37 } 38 39 // Parse detailed flag (default: false) 40 detailed := r.URL.Query().Get("detailed") == "true" 41 42 // Split comma-separated DIDs 43 rawDIDs := strings.Split(didsParam, ",") 44 45 // Trim whitespace and filter out empty DIDs (handles double commas, trailing commas, etc.) 46 dids := make([]string, 0, len(rawDIDs)) 47 for _, did := range rawDIDs { 48 trimmed := strings.TrimSpace(did) 49 if trimmed != "" { 50 dids = append(dids, trimmed) 51 } 52 } 53 54 // Validate we have at least one valid DID 55 if len(dids) == 0 { 56 writeError(w, http.StatusBadRequest, "InvalidRequest", "at least one valid DID is required") 57 return 58 } 59 60 // Get aggregators from service 61 aggs, err := h.service.GetAggregators(r.Context(), dids) 62 if err != nil { 63 handleServiceError(w, err) 64 return 65 } 66 67 // Build response with appropriate view type based on detailed flag 68 response := GetServicesResponse{ 69 Views: make([]interface{}, 0, len(aggs)), 70 } 71 72 for _, agg := range aggs { 73 if detailed { 74 response.Views = append(response.Views, toAggregatorViewDetailed(agg)) 75 } else { 76 response.Views = append(response.Views, toAggregatorView(agg)) 77 } 78 } 79 80 // Return response 81 w.Header().Set("Content-Type", "application/json") 82 w.WriteHeader(http.StatusOK) 83 if err := json.NewEncoder(w).Encode(response); err != nil { 84 log.Printf("ERROR: Failed to encode getServices response: %v", err) 85 } 86} 87 88// GetServicesResponse matches the lexicon output 89type GetServicesResponse struct { 90 Views []interface{} `json:"views"` // Union of aggregatorView | aggregatorViewDetailed 91} 92 93// AggregatorView matches social.coves.aggregator.defs#aggregatorView (without stats) 94type AggregatorView struct { 95 DID string `json:"did"` 96 DisplayName string `json:"displayName"` 97 Description *string `json:"description,omitempty"` 98 Avatar *string `json:"avatar,omitempty"` 99 ConfigSchema interface{} `json:"configSchema,omitempty"` 100 SourceURL *string `json:"sourceUrl,omitempty"` 101 MaintainerDID *string `json:"maintainer,omitempty"` 102 CreatedAt string `json:"createdAt"` 103 RecordUri string `json:"recordUri"` 104} 105 106// AggregatorViewDetailed matches social.coves.aggregator.defs#aggregatorViewDetailed (with stats) 107type AggregatorViewDetailed struct { 108 DID string `json:"did"` 109 DisplayName string `json:"displayName"` 110 Description *string `json:"description,omitempty"` 111 Avatar *string `json:"avatar,omitempty"` 112 ConfigSchema interface{} `json:"configSchema,omitempty"` 113 SourceURL *string `json:"sourceUrl,omitempty"` 114 MaintainerDID *string `json:"maintainer,omitempty"` 115 CreatedAt string `json:"createdAt"` 116 RecordUri string `json:"recordUri"` 117 Stats AggregatorStats `json:"stats"` 118} 119 120// AggregatorStats matches social.coves.aggregator.defs#aggregatorStats 121type AggregatorStats struct { 122 CommunitiesUsing int `json:"communitiesUsing"` 123 PostsCreated int `json:"postsCreated"` 124} 125 126// toAggregatorView converts domain model to basic aggregatorView (no stats) 127func toAggregatorView(agg *aggregators.Aggregator) AggregatorView { 128 view := AggregatorView{ 129 DID: agg.DID, 130 DisplayName: agg.DisplayName, 131 CreatedAt: agg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), 132 RecordUri: agg.RecordURI, 133 } 134 135 // Add optional fields 136 if agg.Description != "" { 137 view.Description = &agg.Description 138 } 139 if agg.AvatarURL != "" { 140 view.Avatar = &agg.AvatarURL 141 } 142 if agg.MaintainerDID != "" { 143 view.MaintainerDID = &agg.MaintainerDID 144 } 145 if agg.SourceURL != "" { 146 view.SourceURL = &agg.SourceURL 147 } 148 if len(agg.ConfigSchema) > 0 { 149 // ConfigSchema is already JSON, unmarshal it for the view 150 var schema interface{} 151 if err := json.Unmarshal(agg.ConfigSchema, &schema); err == nil { 152 view.ConfigSchema = schema 153 } 154 } 155 156 return view 157} 158 159// toAggregatorViewDetailed converts domain model to detailed aggregatorViewDetailed (with stats) 160func toAggregatorViewDetailed(agg *aggregators.Aggregator) AggregatorViewDetailed { 161 view := AggregatorViewDetailed{ 162 DID: agg.DID, 163 DisplayName: agg.DisplayName, 164 CreatedAt: agg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), 165 RecordUri: agg.RecordURI, 166 Stats: AggregatorStats{ 167 CommunitiesUsing: agg.CommunitiesUsing, 168 PostsCreated: agg.PostsCreated, 169 }, 170 } 171 172 // Add optional fields 173 if agg.Description != "" { 174 view.Description = &agg.Description 175 } 176 if agg.AvatarURL != "" { 177 view.Avatar = &agg.AvatarURL 178 } 179 if agg.MaintainerDID != "" { 180 view.MaintainerDID = &agg.MaintainerDID 181 } 182 if agg.SourceURL != "" { 183 view.SourceURL = &agg.SourceURL 184 } 185 if len(agg.ConfigSchema) > 0 { 186 // ConfigSchema is already JSON, unmarshal it for the view 187 var schema interface{} 188 if err := json.Unmarshal(agg.ConfigSchema, &schema); err == nil { 189 view.ConfigSchema = schema 190 } 191 } 192 193 return view 194}