// kafka — a privacy-respecting metasearch engine // Copyright (C) 2026-present metamorphosis-dev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package httpapi import ( "context" "encoding/json" "net/http" "strings" "github.com/metamorphosis-dev/kafka/internal/contracts" "github.com/metamorphosis-dev/kafka/internal/search" "github.com/metamorphosis-dev/kafka/internal/views" ) type Handler struct { searchSvc *search.Service autocompleteSvc func(ctx context.Context, query string) ([]string, error) sourceURL string } func NewHandler(searchSvc *search.Service, autocompleteSuggestions func(ctx context.Context, query string) ([]string, error), sourceURL string) *Handler { return &Handler{ searchSvc: searchSvc, autocompleteSvc: autocompleteSuggestions, sourceURL: sourceURL, } } func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) } // Index renders the homepage with the search box. func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } if err := views.RenderIndex(w, h.sourceURL); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // OpenSearch serves the OpenSearch description XML. func (h *Handler) OpenSearch(baseURL string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { xml, err := views.OpenSearchXML(baseURL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/opensearchdescription+xml; charset=utf-8") w.Write(xml) } } func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { // For HTML format with no query, redirect to homepage. if r.FormValue("q") == "" && (r.FormValue("format") == "" || r.FormValue("format") == "html") { http.Redirect(w, r, "/", http.StatusFound) return } req, err := search.ParseSearchRequest(r) if err != nil { // For HTML, render error on the results page. if req.Format == contracts.FormatHTML || r.FormValue("format") == "html" { pd := views.PageData{SourceURL: h.sourceURL, Query: r.FormValue("q")} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { views.RenderSearch(w, pd) } return } http.Error(w, err.Error(), http.StatusBadRequest) return } resp, err := h.searchSvc.Search(r.Context(), req) if err != nil { if req.Format == contracts.FormatHTML { pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { views.RenderSearch(w, pd) } return } http.Error(w, err.Error(), http.StatusInternalServerError) return } if req.Format == contracts.FormatHTML { pd := views.FromResponse(resp, req.Query, req.Pageno) if err := views.RenderSearchAuto(w, r, pd); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if err := search.WriteSearchResponse(w, req.Format, resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Autocompleter returns search suggestions for the given query. func (h *Handler) Autocompleter(w http.ResponseWriter, r *http.Request) { query := strings.TrimSpace(r.FormValue("q")) if query == "" { w.WriteHeader(http.StatusBadRequest) return } suggestions, err := h.autocompleteSvc(r.Context(), query) if err != nil { // Return empty list on error rather than an error status. suggestions = []string{} } w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(suggestions) }