this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(handler): add source fields to API handlers

Update all API handler methods and types to support source
metadata fields (source_type, source_network, source_channel,
source_user_id, source_user_name) in requests, responses, and
query parameter filtering. Source-scoped duplicate detection is
now used when creating links with source fields provided.

+198 -78
+75 -28
internal/handler/api_v1_links.go
··· 70 70 limit := parseIntParam(r, "limit", 50, 1000) 71 71 offset := parseIntParam(r, "offset", 0, 1000000) 72 72 73 + // Parse source filter query params 74 + var sourceFilter data.SourceFilter 75 + if st := r.URL.Query().Get("source_type"); st != "" { 76 + sourceFilter.SourceType = &st 77 + } 78 + if sn := r.URL.Query().Get("source_network"); sn != "" { 79 + sourceFilter.SourceNetwork = &sn 80 + } 81 + if sc := r.URL.Query().Get("source_channel"); sc != "" { 82 + sourceFilter.SourceChannel = &sc 83 + } 84 + 73 85 // Fetch all links from the last year 74 86 // We fetch more than needed so we can paginate in-memory 75 - links, err := h.Store.GetRecentIRCLinks(ctx, 365, 0, data.SourceFilter{}) 87 + links, err := h.Store.GetRecentIRCLinks(ctx, 365, 0, sourceFilter) 76 88 if err != nil { 77 89 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch links") 78 90 return ··· 96 108 data := make([]APILinkResponse, 0, len(links)) 97 109 for _, link := range links { 98 110 data = append(data, APILinkResponse{ 99 - ID: link.ID, 100 - URL: link.URL, 101 - Title: link.Title, 102 - User: link.User, 103 - Clicks: link.Clicks, 104 - CreatedAt: link.Timestamp, 105 - Tags: h.getTagStrings(ctx, "link", link.ID), 111 + ID: link.ID, 112 + URL: link.URL, 113 + Title: link.Title, 114 + User: link.User, 115 + Clicks: link.Clicks, 116 + CreatedAt: link.Timestamp, 117 + Tags: h.getTagStrings(ctx, "link", link.ID), 118 + SourceType: link.SourceType, 119 + SourceNetwork: link.SourceNetwork, 120 + SourceChannel: link.SourceChannel, 121 + SourceUserID: link.SourceUserID, 122 + SourceUserName: link.SourceUserName, 106 123 }) 107 124 } 108 125 ··· 120 137 121 138 // APILinkCreateRequest is the request body for POST /api/v1/links. 122 139 type APILinkCreateRequest struct { 123 - URL string `json:"url"` 124 - User string `json:"user"` 125 - Tags []string `json:"tags,omitempty"` 140 + URL string `json:"url"` 141 + User string `json:"user"` 142 + Tags []string `json:"tags,omitempty"` 143 + SourceType *string `json:"source_type,omitempty"` 144 + SourceNetwork *string `json:"source_network,omitempty"` 145 + SourceChannel *string `json:"source_channel,omitempty"` 146 + SourceUserID *string `json:"source_user_id,omitempty"` 147 + SourceUserName *string `json:"source_user_name,omitempty"` 126 148 } 127 149 128 150 // apiV1CreateLink handles POST /api/v1/links ··· 152 174 return 153 175 } 154 176 155 - // Check for duplicates 156 - existingLinks, err := h.Store.GetIRCLinksByURL(ctx, req.URL, data.SourceFilter{}) 177 + // Check for duplicates (scoped by source if provided) 178 + sourceFilter := data.SourceFilter{ 179 + SourceType: req.SourceType, 180 + SourceNetwork: req.SourceNetwork, 181 + SourceChannel: req.SourceChannel, 182 + } 183 + existingLinks, err := h.Store.GetIRCLinksByURL(ctx, req.URL, sourceFilter) 157 184 if err != nil { 158 185 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to check for duplicates") 159 186 return 160 187 } 161 188 162 189 // Insert the link (use URL as title for now; existing code fetches title async) 163 - linkID, err := h.Store.InsertIRCLink(ctx, &data.IRCLink{User: req.User, Title: req.URL, URL: req.URL, ContentType: ""}) 190 + linkID, err := h.Store.InsertIRCLink(ctx, &data.IRCLink{ 191 + User: req.User, 192 + Title: req.URL, 193 + URL: req.URL, 194 + ContentType: "", 195 + SourceType: req.SourceType, 196 + SourceNetwork: req.SourceNetwork, 197 + SourceChannel: req.SourceChannel, 198 + SourceUserID: req.SourceUserID, 199 + SourceUserName: req.SourceUserName, 200 + }) 164 201 if err != nil { 165 202 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to create link") 166 203 return ··· 206 243 207 244 resp := APILinkCreateResponse{ 208 245 APILinkResponse: APILinkResponse{ 209 - ID: linkID, 210 - URL: req.URL, 211 - Title: req.URL, 212 - User: req.User, 213 - Clicks: 0, 214 - CreatedAt: time.Now(), 215 - Tags: tagStrings, 246 + ID: linkID, 247 + URL: req.URL, 248 + Title: req.URL, 249 + User: req.User, 250 + Clicks: 0, 251 + CreatedAt: time.Now(), 252 + Tags: tagStrings, 253 + SourceType: req.SourceType, 254 + SourceNetwork: req.SourceNetwork, 255 + SourceChannel: req.SourceChannel, 256 + SourceUserID: req.SourceUserID, 257 + SourceUserName: req.SourceUserName, 216 258 }, 217 259 IsDuplicate: isDuplicate, 218 260 PreviousSubmissions: previousSubmissions, ··· 244 286 } 245 287 246 288 writeJSON(w, http.StatusOK, APILinkResponse{ 247 - ID: link.ID, 248 - URL: link.URL, 249 - Title: link.Title, 250 - User: link.User, 251 - Clicks: link.Clicks, 252 - CreatedAt: link.Timestamp, 253 - Tags: h.getTagStrings(ctx, "link", link.ID), 289 + ID: link.ID, 290 + URL: link.URL, 291 + Title: link.Title, 292 + User: link.User, 293 + Clicks: link.Clicks, 294 + CreatedAt: link.Timestamp, 295 + Tags: h.getTagStrings(ctx, "link", link.ID), 296 + SourceType: link.SourceType, 297 + SourceNetwork: link.SourceNetwork, 298 + SourceChannel: link.SourceChannel, 299 + SourceUserID: link.SourceUserID, 300 + SourceUserName: link.SourceUserName, 254 301 }) 255 302 } 256 303
+65 -24
internal/handler/api_v1_quotes.go
··· 70 70 limit := parseIntParam(r, "limit", 50, 1000) 71 71 offset := parseIntParam(r, "offset", 0, 1000000) 72 72 73 + // Parse source filter query params 74 + var sourceFilter data.SourceFilter 75 + if st := r.URL.Query().Get("source_type"); st != "" { 76 + sourceFilter.SourceType = &st 77 + } 78 + if sn := r.URL.Query().Get("source_network"); sn != "" { 79 + sourceFilter.SourceNetwork = &sn 80 + } 81 + if sc := r.URL.Query().Get("source_channel"); sc != "" { 82 + sourceFilter.SourceChannel = &sc 83 + } 84 + 73 85 // Fetch all quotes from the last year 74 86 // We fetch more than needed so we can paginate in-memory 75 - quotes, err := h.Store.GetRecentQuotes(ctx, 365, 0, data.SourceFilter{}) 87 + quotes, err := h.Store.GetRecentQuotes(ctx, 365, 0, sourceFilter) 76 88 if err != nil { 77 89 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch quotes") 78 90 return ··· 96 108 data := make([]APIQuoteResponse, 0, len(quotes)) 97 109 for _, quote := range quotes { 98 110 data = append(data, APIQuoteResponse{ 99 - ID: quote.ID, 100 - Quote: quote.Quote, 101 - Author: quote.Author, 102 - Poster: quote.Poster, 103 - CreatedAt: quote.Timestamp, 104 - Tags: h.getTagStrings(ctx, "quote", quote.ID), 111 + ID: quote.ID, 112 + Quote: quote.Quote, 113 + Author: quote.Author, 114 + Poster: quote.Poster, 115 + CreatedAt: quote.Timestamp, 116 + Tags: h.getTagStrings(ctx, "quote", quote.ID), 117 + SourceType: quote.SourceType, 118 + SourceNetwork: quote.SourceNetwork, 119 + SourceChannel: quote.SourceChannel, 120 + SourceUserID: quote.SourceUserID, 121 + SourceUserName: quote.SourceUserName, 105 122 }) 106 123 } 107 124 ··· 119 136 120 137 // APIQuoteCreateRequest is the request body for POST /api/v1/quotes. 121 138 type APIQuoteCreateRequest struct { 122 - Quote string `json:"quote"` 123 - Author string `json:"author"` 124 - Poster string `json:"poster"` 125 - Tags []string `json:"tags,omitempty"` 139 + Quote string `json:"quote"` 140 + Author string `json:"author"` 141 + Poster string `json:"poster"` 142 + Tags []string `json:"tags,omitempty"` 143 + SourceType *string `json:"source_type,omitempty"` 144 + SourceNetwork *string `json:"source_network,omitempty"` 145 + SourceChannel *string `json:"source_channel,omitempty"` 146 + SourceUserID *string `json:"source_user_id,omitempty"` 147 + SourceUserName *string `json:"source_user_name,omitempty"` 126 148 } 127 149 128 150 // apiV1CreateQuote handles POST /api/v1/quotes ··· 148 170 } 149 171 150 172 // Insert the quote 151 - quoteID, err := h.Store.InsertQuote(ctx, &data.Quote{Quote: req.Quote, Author: req.Author, Poster: req.Poster}) 173 + quoteID, err := h.Store.InsertQuote(ctx, &data.Quote{ 174 + Quote: req.Quote, 175 + Author: req.Author, 176 + Poster: req.Poster, 177 + SourceType: req.SourceType, 178 + SourceNetwork: req.SourceNetwork, 179 + SourceChannel: req.SourceChannel, 180 + SourceUserID: req.SourceUserID, 181 + SourceUserName: req.SourceUserName, 182 + }) 152 183 if err != nil { 153 184 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to create quote") 154 185 return ··· 181 212 } 182 213 183 214 resp := APIQuoteResponse{ 184 - ID: quoteID, 185 - Quote: req.Quote, 186 - Author: req.Author, 187 - Poster: req.Poster, 188 - CreatedAt: time.Now(), 189 - Tags: tagStrings, 215 + ID: quoteID, 216 + Quote: req.Quote, 217 + Author: req.Author, 218 + Poster: req.Poster, 219 + CreatedAt: time.Now(), 220 + Tags: tagStrings, 221 + SourceType: req.SourceType, 222 + SourceNetwork: req.SourceNetwork, 223 + SourceChannel: req.SourceChannel, 224 + SourceUserID: req.SourceUserID, 225 + SourceUserName: req.SourceUserName, 190 226 } 191 227 192 228 writeJSON(w, http.StatusCreated, resp) ··· 219 255 } 220 256 221 257 writeJSON(w, http.StatusOK, APIQuoteResponse{ 222 - ID: quote.ID, 223 - Quote: quote.Quote, 224 - Author: quote.Author, 225 - Poster: quote.Poster, 226 - CreatedAt: quote.Timestamp, 227 - Tags: h.getTagStrings(ctx, "quote", quote.ID), 258 + ID: quote.ID, 259 + Quote: quote.Quote, 260 + Author: quote.Author, 261 + Poster: quote.Poster, 262 + CreatedAt: quote.Timestamp, 263 + Tags: h.getTagStrings(ctx, "quote", quote.ID), 264 + SourceType: quote.SourceType, 265 + SourceNetwork: quote.SourceNetwork, 266 + SourceChannel: quote.SourceChannel, 267 + SourceUserID: quote.SourceUserID, 268 + SourceUserName: quote.SourceUserName, 228 269 }) 229 270 } 230 271
+35 -13
internal/handler/api_v1_search.go
··· 63 63 limit := parseIntParam(r, "limit", 50, 1000) 64 64 offset := parseIntParam(r, "offset", 0, 1000000) 65 65 66 + // Parse source filter query params 67 + var sourceFilter data.SourceFilter 68 + if st := r.URL.Query().Get("source_type"); st != "" { 69 + sourceFilter.SourceType = &st 70 + } 71 + if sn := r.URL.Query().Get("source_network"); sn != "" { 72 + sourceFilter.SourceNetwork = &sn 73 + } 74 + if sc := r.URL.Query().Get("source_channel"); sc != "" { 75 + sourceFilter.SourceChannel = &sc 76 + } 77 + 66 78 // Initialize response 67 79 resp := APISearchResponse{ 68 80 Links: []APILinkResponse{}, ··· 80 92 81 93 // Search links if requested 82 94 if searchLinks { 83 - links, err := h.Store.SearchIRCLinks(ctx, query, data.SourceFilter{}) 95 + links, err := h.Store.SearchIRCLinks(ctx, query, sourceFilter) 84 96 if err != nil { 85 97 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to search links") 86 98 return ··· 104 116 // Convert to API response format 105 117 for _, link := range links { 106 118 resp.Links = append(resp.Links, APILinkResponse{ 107 - ID: link.ID, 108 - URL: link.URL, 109 - Title: link.Title, 110 - User: link.User, 111 - Clicks: link.Clicks, 112 - CreatedAt: link.Timestamp, 119 + ID: link.ID, 120 + URL: link.URL, 121 + Title: link.Title, 122 + User: link.User, 123 + Clicks: link.Clicks, 124 + CreatedAt: link.Timestamp, 125 + SourceType: link.SourceType, 126 + SourceNetwork: link.SourceNetwork, 127 + SourceChannel: link.SourceChannel, 128 + SourceUserID: link.SourceUserID, 129 + SourceUserName: link.SourceUserName, 113 130 }) 114 131 } 115 132 } 116 133 117 134 // Search quotes if requested 118 135 if searchQuotes { 119 - quotes, err := h.Store.SearchQuotes(ctx, query, data.SourceFilter{}) 136 + quotes, err := h.Store.SearchQuotes(ctx, query, sourceFilter) 120 137 if err != nil { 121 138 writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to search quotes") 122 139 return ··· 140 157 // Convert to API response format 141 158 for _, quote := range quotes { 142 159 resp.Quotes = append(resp.Quotes, APIQuoteResponse{ 143 - ID: quote.ID, 144 - Quote: quote.Quote, 145 - Author: quote.Author, 146 - Poster: quote.Poster, 147 - CreatedAt: quote.Timestamp, 160 + ID: quote.ID, 161 + Quote: quote.Quote, 162 + Author: quote.Author, 163 + Poster: quote.Poster, 164 + CreatedAt: quote.Timestamp, 165 + SourceType: quote.SourceType, 166 + SourceNetwork: quote.SourceNetwork, 167 + SourceChannel: quote.SourceChannel, 168 + SourceUserID: quote.SourceUserID, 169 + SourceUserName: quote.SourceUserName, 148 170 }) 149 171 } 150 172 }
+23 -13
internal/handler/api_v1_types.go
··· 25 25 26 26 // APILinkResponse represents a single link in API responses. 27 27 type APILinkResponse struct { 28 - ID int `json:"id"` 29 - URL string `json:"url"` 30 - Title string `json:"title"` 31 - User string `json:"user"` 32 - Clicks int `json:"clicks"` 33 - CreatedAt time.Time `json:"created_at"` 34 - Tags []string `json:"tags,omitempty"` 28 + ID int `json:"id"` 29 + URL string `json:"url"` 30 + Title string `json:"title"` 31 + User string `json:"user"` 32 + Clicks int `json:"clicks"` 33 + CreatedAt time.Time `json:"created_at"` 34 + Tags []string `json:"tags,omitempty"` 35 + SourceType *string `json:"source_type,omitempty"` 36 + SourceNetwork *string `json:"source_network,omitempty"` 37 + SourceChannel *string `json:"source_channel,omitempty"` 38 + SourceUserID *string `json:"source_user_id,omitempty"` 39 + SourceUserName *string `json:"source_user_name,omitempty"` 35 40 } 36 41 37 42 // APIPreviousSubmission contains information about a previous submission ··· 58 63 59 64 // APIQuoteResponse represents a single quote in API responses. 60 65 type APIQuoteResponse struct { 61 - ID int `json:"id"` 62 - Quote string `json:"quote"` 63 - Author string `json:"author"` 64 - Poster string `json:"poster,omitempty"` 65 - CreatedAt time.Time `json:"created_at"` 66 - Tags []string `json:"tags,omitempty"` 66 + ID int `json:"id"` 67 + Quote string `json:"quote"` 68 + Author string `json:"author"` 69 + Poster string `json:"poster,omitempty"` 70 + CreatedAt time.Time `json:"created_at"` 71 + Tags []string `json:"tags,omitempty"` 72 + SourceType *string `json:"source_type,omitempty"` 73 + SourceNetwork *string `json:"source_network,omitempty"` 74 + SourceChannel *string `json:"source_channel,omitempty"` 75 + SourceUserID *string `json:"source_user_id,omitempty"` 76 + SourceUserName *string `json:"source_user_name,omitempty"` 67 77 } 68 78 69 79 // APIQuotesResponse is the paginated response for a list of quotes.