package web import ( "embed" "fmt" "html/template" "io/fs" "net/http" "net/url" "path/filepath" "strconv" "strings" "github.com/go-chi/chi/v5" "github.com/squid/rcs/backend/internal/config" "github.com/squid/rcs/backend/internal/db" "github.com/squid/rcs/backend/internal/imgur" ) const pageSize = 50 //go:embed templates/* static/* var assets embed.FS // UI serves the search Web UI. type UI struct { cfg config.Config store *db.Store imgur *imgur.Client tmpl *template.Template } // New creates the Web UI. func New(cfg config.Config, store *db.Store, imgurClient *imgur.Client) (*UI, error) { tmpl, err := template.New("").Funcs(template.FuncMap{ "hasImgur": func(url string) bool { return strings.TrimSpace(url) != "" }, "snippet": func(body string) string { body = strings.TrimSpace(body) if len(body) > 280 { return body[:280] + "…" } return body }, "screenshotURL": func(path string) string { if path == "" { return "" } return "/screenshots/" + filepath.Base(path) }, "pageURL": func(data pageData, page int) string { values := url.Values{} if data.Query != "" { values.Set("q", data.Query) } if data.User != "" { values.Set("user", data.User) } if page > 1 { values.Set("page", strconv.Itoa(page)) } encoded := values.Encode() if encoded == "" { return "/" } return "/?" + encoded }, }).ParseFS(assets, "templates/*.html") if err != nil { return nil, fmt.Errorf("parse templates: %w", err) } return &UI{cfg: cfg, store: store, imgur: imgurClient, tmpl: tmpl}, nil } // StaticHandler serves embedded CSS/JS. func (u *UI) StaticHandler() http.Handler { sub, err := fs.Sub(assets, "static") if err != nil { return http.NotFoundHandler() } return http.StripPrefix("/static/", http.FileServer(http.FS(sub))) } type pageData struct { Query string User string Results []db.SearchResult Error string Flash string ImgurOn bool Page int TotalPages int Total int HasPrev bool HasNext bool PrevPage int NextPage int } func parsePage(raw string) int { page, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || page < 1 { return 1 } return page } // HandleIndex renders search results. func (u *UI) HandleIndex(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) user := strings.TrimSpace(r.URL.Query().Get("user")) flash := strings.TrimSpace(r.URL.Query().Get("flash")) errMsg := strings.TrimSpace(r.URL.Query().Get("error")) page := parsePage(r.URL.Query().Get("page")) data := pageData{ Query: q, User: user, Flash: flash, Error: errMsg, ImgurOn: u.imgur.Enabled(), Page: page, } total, err := u.store.CountComments(r.Context(), q, user) if err != nil { data.Error = err.Error() } else { data.Total = total data.TotalPages = total / pageSize if total%pageSize != 0 { data.TotalPages++ } if data.TotalPages == 0 { data.TotalPages = 1 } if page > data.TotalPages { page = data.TotalPages data.Page = page } data.HasPrev = page > 1 data.HasNext = page < data.TotalPages data.PrevPage = page - 1 data.NextPage = page + 1 offset := (page - 1) * pageSize results, err := u.store.SearchComments(r.Context(), q, user, pageSize, offset) if err != nil { data.Error = err.Error() } else { data.Results = results } } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := u.tmpl.ExecuteTemplate(w, "index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // HandleExport uploads a comment screenshot to Imgur (or returns existing URL). func (u *UI) HandleExport(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") q := strings.TrimSpace(r.FormValue("q")) if q == "" { q = strings.TrimSpace(r.URL.Query().Get("q")) } user := strings.TrimSpace(r.FormValue("user")) if user == "" { user = strings.TrimSpace(r.URL.Query().Get("user")) } pageRaw := strings.TrimSpace(r.FormValue("page")) if pageRaw == "" { pageRaw = strings.TrimSpace(r.URL.Query().Get("page")) } page := parsePage(pageRaw) redirect := func(flash, errMsg string) { values := url.Values{} if q != "" { values.Set("q", q) } if user != "" { values.Set("user", user) } if page > 1 { values.Set("page", strconv.Itoa(page)) } if flash != "" { values.Set("flash", flash) } if errMsg != "" { values.Set("error", errMsg) } target := "/" if encoded := values.Encode(); encoded != "" { target += "?" + encoded } http.Redirect(w, r, target, http.StatusSeeOther) } comment, err := u.store.GetCommentByRedditID(r.Context(), id) if err != nil { redirect("", err.Error()) return } if comment == nil { redirect("", "comment not found") return } if comment.ImgurURL != "" { redirect("Already exported", "") return } if comment.ScreenshotPath == "" { redirect("", "no screenshot for this comment") return } absPath := comment.ScreenshotPath if !filepath.IsAbs(absPath) { absPath = filepath.Join(u.cfg.DataDir, absPath) } link, err := u.imgur.UploadFile(absPath) if err != nil { redirect("", err.Error()) return } if err := u.store.SetImgurURL(r.Context(), id, link); err != nil { redirect("", err.Error()) return } redirect("Exported to Imgur", "") }