diff --git a/internal/handler/api_v1_links.go b/internal/handler/api_v1_links.go index 94f5c37..e949775 100644 --- a/internal/handler/api_v1_links.go +++ b/internal/handler/api_v1_links.go @@ -70,9 +70,21 @@ func (h *Handler) apiV1ListLinks(w http.ResponseWriter, r *http.Request) { limit := parseIntParam(r, "limit", 50, 1000) offset := parseIntParam(r, "offset", 0, 1000000) + // Parse source filter query params + var sourceFilter data.SourceFilter + if st := r.URL.Query().Get("source_type"); st != "" { + sourceFilter.SourceType = &st + } + if sn := r.URL.Query().Get("source_network"); sn != "" { + sourceFilter.SourceNetwork = &sn + } + if sc := r.URL.Query().Get("source_channel"); sc != "" { + sourceFilter.SourceChannel = &sc + } + // Fetch all links from the last year // We fetch more than needed so we can paginate in-memory - links, err := h.Store.GetRecentIRCLinks(ctx, 365, 0, data.SourceFilter{}) + links, err := h.Store.GetRecentIRCLinks(ctx, 365, 0, sourceFilter) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch links") return @@ -96,13 +108,18 @@ func (h *Handler) apiV1ListLinks(w http.ResponseWriter, r *http.Request) { data := make([]APILinkResponse, 0, len(links)) for _, link := range links { data = append(data, APILinkResponse{ - ID: link.ID, - URL: link.URL, - Title: link.Title, - User: link.User, - Clicks: link.Clicks, - CreatedAt: link.Timestamp, - Tags: h.getTagStrings(ctx, "link", link.ID), + ID: link.ID, + URL: link.URL, + Title: link.Title, + User: link.User, + Clicks: link.Clicks, + CreatedAt: link.Timestamp, + Tags: h.getTagStrings(ctx, "link", link.ID), + SourceType: link.SourceType, + SourceNetwork: link.SourceNetwork, + SourceChannel: link.SourceChannel, + SourceUserID: link.SourceUserID, + SourceUserName: link.SourceUserName, }) } @@ -120,9 +137,14 @@ func (h *Handler) apiV1ListLinks(w http.ResponseWriter, r *http.Request) { // APILinkCreateRequest is the request body for POST /api/v1/links. type APILinkCreateRequest struct { - URL string `json:"url"` - User string `json:"user"` - Tags []string `json:"tags,omitempty"` + URL string `json:"url"` + User string `json:"user"` + Tags []string `json:"tags,omitempty"` + SourceType *string `json:"source_type,omitempty"` + SourceNetwork *string `json:"source_network,omitempty"` + SourceChannel *string `json:"source_channel,omitempty"` + SourceUserID *string `json:"source_user_id,omitempty"` + SourceUserName *string `json:"source_user_name,omitempty"` } // apiV1CreateLink handles POST /api/v1/links @@ -152,15 +174,30 @@ func (h *Handler) apiV1CreateLink(w http.ResponseWriter, r *http.Request) { return } - // Check for duplicates - existingLinks, err := h.Store.GetIRCLinksByURL(ctx, req.URL, data.SourceFilter{}) + // Check for duplicates (scoped by source if provided) + sourceFilter := data.SourceFilter{ + SourceType: req.SourceType, + SourceNetwork: req.SourceNetwork, + SourceChannel: req.SourceChannel, + } + existingLinks, err := h.Store.GetIRCLinksByURL(ctx, req.URL, sourceFilter) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to check for duplicates") return } // Insert the link (use URL as title for now; existing code fetches title async) - linkID, err := h.Store.InsertIRCLink(ctx, &data.IRCLink{User: req.User, Title: req.URL, URL: req.URL, ContentType: ""}) + linkID, err := h.Store.InsertIRCLink(ctx, &data.IRCLink{ + User: req.User, + Title: req.URL, + URL: req.URL, + ContentType: "", + SourceType: req.SourceType, + SourceNetwork: req.SourceNetwork, + SourceChannel: req.SourceChannel, + SourceUserID: req.SourceUserID, + SourceUserName: req.SourceUserName, + }) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to create link") return @@ -206,13 +243,18 @@ func (h *Handler) apiV1CreateLink(w http.ResponseWriter, r *http.Request) { resp := APILinkCreateResponse{ APILinkResponse: APILinkResponse{ - ID: linkID, - URL: req.URL, - Title: req.URL, - User: req.User, - Clicks: 0, - CreatedAt: time.Now(), - Tags: tagStrings, + ID: linkID, + URL: req.URL, + Title: req.URL, + User: req.User, + Clicks: 0, + CreatedAt: time.Now(), + Tags: tagStrings, + SourceType: req.SourceType, + SourceNetwork: req.SourceNetwork, + SourceChannel: req.SourceChannel, + SourceUserID: req.SourceUserID, + SourceUserName: req.SourceUserName, }, IsDuplicate: isDuplicate, PreviousSubmissions: previousSubmissions, @@ -244,13 +286,18 @@ func (h *Handler) apiV1GetLink(w http.ResponseWriter, r *http.Request, id int) { } writeJSON(w, http.StatusOK, APILinkResponse{ - ID: link.ID, - URL: link.URL, - Title: link.Title, - User: link.User, - Clicks: link.Clicks, - CreatedAt: link.Timestamp, - Tags: h.getTagStrings(ctx, "link", link.ID), + ID: link.ID, + URL: link.URL, + Title: link.Title, + User: link.User, + Clicks: link.Clicks, + CreatedAt: link.Timestamp, + Tags: h.getTagStrings(ctx, "link", link.ID), + SourceType: link.SourceType, + SourceNetwork: link.SourceNetwork, + SourceChannel: link.SourceChannel, + SourceUserID: link.SourceUserID, + SourceUserName: link.SourceUserName, }) } diff --git a/internal/handler/api_v1_quotes.go b/internal/handler/api_v1_quotes.go index 69dcd6a..7fb7f81 100644 --- a/internal/handler/api_v1_quotes.go +++ b/internal/handler/api_v1_quotes.go @@ -70,9 +70,21 @@ func (h *Handler) apiV1ListQuotes(w http.ResponseWriter, r *http.Request) { limit := parseIntParam(r, "limit", 50, 1000) offset := parseIntParam(r, "offset", 0, 1000000) + // Parse source filter query params + var sourceFilter data.SourceFilter + if st := r.URL.Query().Get("source_type"); st != "" { + sourceFilter.SourceType = &st + } + if sn := r.URL.Query().Get("source_network"); sn != "" { + sourceFilter.SourceNetwork = &sn + } + if sc := r.URL.Query().Get("source_channel"); sc != "" { + sourceFilter.SourceChannel = &sc + } + // Fetch all quotes from the last year // We fetch more than needed so we can paginate in-memory - quotes, err := h.Store.GetRecentQuotes(ctx, 365, 0, data.SourceFilter{}) + quotes, err := h.Store.GetRecentQuotes(ctx, 365, 0, sourceFilter) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch quotes") return @@ -96,12 +108,17 @@ func (h *Handler) apiV1ListQuotes(w http.ResponseWriter, r *http.Request) { data := make([]APIQuoteResponse, 0, len(quotes)) for _, quote := range quotes { data = append(data, APIQuoteResponse{ - ID: quote.ID, - Quote: quote.Quote, - Author: quote.Author, - Poster: quote.Poster, - CreatedAt: quote.Timestamp, - Tags: h.getTagStrings(ctx, "quote", quote.ID), + ID: quote.ID, + Quote: quote.Quote, + Author: quote.Author, + Poster: quote.Poster, + CreatedAt: quote.Timestamp, + Tags: h.getTagStrings(ctx, "quote", quote.ID), + SourceType: quote.SourceType, + SourceNetwork: quote.SourceNetwork, + SourceChannel: quote.SourceChannel, + SourceUserID: quote.SourceUserID, + SourceUserName: quote.SourceUserName, }) } @@ -119,10 +136,15 @@ func (h *Handler) apiV1ListQuotes(w http.ResponseWriter, r *http.Request) { // APIQuoteCreateRequest is the request body for POST /api/v1/quotes. type APIQuoteCreateRequest struct { - Quote string `json:"quote"` - Author string `json:"author"` - Poster string `json:"poster"` - Tags []string `json:"tags,omitempty"` + Quote string `json:"quote"` + Author string `json:"author"` + Poster string `json:"poster"` + Tags []string `json:"tags,omitempty"` + SourceType *string `json:"source_type,omitempty"` + SourceNetwork *string `json:"source_network,omitempty"` + SourceChannel *string `json:"source_channel,omitempty"` + SourceUserID *string `json:"source_user_id,omitempty"` + SourceUserName *string `json:"source_user_name,omitempty"` } // apiV1CreateQuote handles POST /api/v1/quotes @@ -148,7 +170,16 @@ func (h *Handler) apiV1CreateQuote(w http.ResponseWriter, r *http.Request) { } // Insert the quote - quoteID, err := h.Store.InsertQuote(ctx, &data.Quote{Quote: req.Quote, Author: req.Author, Poster: req.Poster}) + quoteID, err := h.Store.InsertQuote(ctx, &data.Quote{ + Quote: req.Quote, + Author: req.Author, + Poster: req.Poster, + SourceType: req.SourceType, + SourceNetwork: req.SourceNetwork, + SourceChannel: req.SourceChannel, + SourceUserID: req.SourceUserID, + SourceUserName: req.SourceUserName, + }) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to create quote") return @@ -181,12 +212,17 @@ func (h *Handler) apiV1CreateQuote(w http.ResponseWriter, r *http.Request) { } resp := APIQuoteResponse{ - ID: quoteID, - Quote: req.Quote, - Author: req.Author, - Poster: req.Poster, - CreatedAt: time.Now(), - Tags: tagStrings, + ID: quoteID, + Quote: req.Quote, + Author: req.Author, + Poster: req.Poster, + CreatedAt: time.Now(), + Tags: tagStrings, + SourceType: req.SourceType, + SourceNetwork: req.SourceNetwork, + SourceChannel: req.SourceChannel, + SourceUserID: req.SourceUserID, + SourceUserName: req.SourceUserName, } writeJSON(w, http.StatusCreated, resp) @@ -219,12 +255,17 @@ func (h *Handler) apiV1GetQuote(w http.ResponseWriter, r *http.Request, id int) } writeJSON(w, http.StatusOK, APIQuoteResponse{ - ID: quote.ID, - Quote: quote.Quote, - Author: quote.Author, - Poster: quote.Poster, - CreatedAt: quote.Timestamp, - Tags: h.getTagStrings(ctx, "quote", quote.ID), + ID: quote.ID, + Quote: quote.Quote, + Author: quote.Author, + Poster: quote.Poster, + CreatedAt: quote.Timestamp, + Tags: h.getTagStrings(ctx, "quote", quote.ID), + SourceType: quote.SourceType, + SourceNetwork: quote.SourceNetwork, + SourceChannel: quote.SourceChannel, + SourceUserID: quote.SourceUserID, + SourceUserName: quote.SourceUserName, }) } diff --git a/internal/handler/api_v1_search.go b/internal/handler/api_v1_search.go index a3d6efb..2025993 100644 --- a/internal/handler/api_v1_search.go +++ b/internal/handler/api_v1_search.go @@ -63,6 +63,18 @@ func (h *Handler) APIv1SearchHandler(w http.ResponseWriter, r *http.Request) { limit := parseIntParam(r, "limit", 50, 1000) offset := parseIntParam(r, "offset", 0, 1000000) + // Parse source filter query params + var sourceFilter data.SourceFilter + if st := r.URL.Query().Get("source_type"); st != "" { + sourceFilter.SourceType = &st + } + if sn := r.URL.Query().Get("source_network"); sn != "" { + sourceFilter.SourceNetwork = &sn + } + if sc := r.URL.Query().Get("source_channel"); sc != "" { + sourceFilter.SourceChannel = &sc + } + // Initialize response resp := APISearchResponse{ Links: []APILinkResponse{}, @@ -80,7 +92,7 @@ func (h *Handler) APIv1SearchHandler(w http.ResponseWriter, r *http.Request) { // Search links if requested if searchLinks { - links, err := h.Store.SearchIRCLinks(ctx, query, data.SourceFilter{}) + links, err := h.Store.SearchIRCLinks(ctx, query, sourceFilter) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to search links") return @@ -104,19 +116,24 @@ func (h *Handler) APIv1SearchHandler(w http.ResponseWriter, r *http.Request) { // Convert to API response format for _, link := range links { resp.Links = append(resp.Links, APILinkResponse{ - ID: link.ID, - URL: link.URL, - Title: link.Title, - User: link.User, - Clicks: link.Clicks, - CreatedAt: link.Timestamp, + ID: link.ID, + URL: link.URL, + Title: link.Title, + User: link.User, + Clicks: link.Clicks, + CreatedAt: link.Timestamp, + SourceType: link.SourceType, + SourceNetwork: link.SourceNetwork, + SourceChannel: link.SourceChannel, + SourceUserID: link.SourceUserID, + SourceUserName: link.SourceUserName, }) } } // Search quotes if requested if searchQuotes { - quotes, err := h.Store.SearchQuotes(ctx, query, data.SourceFilter{}) + quotes, err := h.Store.SearchQuotes(ctx, query, sourceFilter) if err != nil { writeAPIError(w, http.StatusInternalServerError, "internal_error", "Failed to search quotes") return @@ -140,11 +157,16 @@ func (h *Handler) APIv1SearchHandler(w http.ResponseWriter, r *http.Request) { // Convert to API response format for _, quote := range quotes { resp.Quotes = append(resp.Quotes, APIQuoteResponse{ - ID: quote.ID, - Quote: quote.Quote, - Author: quote.Author, - Poster: quote.Poster, - CreatedAt: quote.Timestamp, + ID: quote.ID, + Quote: quote.Quote, + Author: quote.Author, + Poster: quote.Poster, + CreatedAt: quote.Timestamp, + SourceType: quote.SourceType, + SourceNetwork: quote.SourceNetwork, + SourceChannel: quote.SourceChannel, + SourceUserID: quote.SourceUserID, + SourceUserName: quote.SourceUserName, }) } } diff --git a/internal/handler/api_v1_types.go b/internal/handler/api_v1_types.go index 3b496ae..0436c62 100644 --- a/internal/handler/api_v1_types.go +++ b/internal/handler/api_v1_types.go @@ -25,13 +25,18 @@ type APIMeta struct { // APILinkResponse represents a single link in API responses. type APILinkResponse struct { - ID int `json:"id"` - URL string `json:"url"` - Title string `json:"title"` - User string `json:"user"` - Clicks int `json:"clicks"` - CreatedAt time.Time `json:"created_at"` - Tags []string `json:"tags,omitempty"` + ID int `json:"id"` + URL string `json:"url"` + Title string `json:"title"` + User string `json:"user"` + Clicks int `json:"clicks"` + CreatedAt time.Time `json:"created_at"` + Tags []string `json:"tags,omitempty"` + SourceType *string `json:"source_type,omitempty"` + SourceNetwork *string `json:"source_network,omitempty"` + SourceChannel *string `json:"source_channel,omitempty"` + SourceUserID *string `json:"source_user_id,omitempty"` + SourceUserName *string `json:"source_user_name,omitempty"` } // APIPreviousSubmission contains information about a previous submission @@ -58,12 +63,17 @@ type APILinksResponse struct { // APIQuoteResponse represents a single quote in API responses. type APIQuoteResponse struct { - ID int `json:"id"` - Quote string `json:"quote"` - Author string `json:"author"` - Poster string `json:"poster,omitempty"` - CreatedAt time.Time `json:"created_at"` - Tags []string `json:"tags,omitempty"` + ID int `json:"id"` + Quote string `json:"quote"` + Author string `json:"author"` + Poster string `json:"poster,omitempty"` + CreatedAt time.Time `json:"created_at"` + Tags []string `json:"tags,omitempty"` + SourceType *string `json:"source_type,omitempty"` + SourceNetwork *string `json:"source_network,omitempty"` + SourceChannel *string `json:"source_channel,omitempty"` + SourceUserID *string `json:"source_user_id,omitempty"` + SourceUserName *string `json:"source_user_name,omitempty"` } // APIQuotesResponse is the paginated response for a list of quotes.