package handlers import ( "fmt" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) func ListPosts(w http.ResponseWriter, r *http.Request) { listPostsQuery(w, r, false) } func ListPostsRecommended(w http.ResponseWriter, r *http.Request) { pool := poolFrom(r) limit := postLimitDef(r, 12, 50) rows, err := pool.Query(r.Context(), postSelectBase()+` WHERE `+publicPostWhere+` AND p.is_recommended = TRUE ORDER BY p.sort_order ASC, p.published_at DESC NULLS LAST, p.id DESC LIMIT $1`, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } items, err := collectPostRows(r, rows) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]any{"items": items}) } func ListPostsLatest(w http.ResponseWriter, r *http.Request) { pool := poolFrom(r) limit := postLimitDef(r, 8, 50) rows, err := pool.Query(r.Context(), postSelectBase()+` WHERE `+publicPostWhere+` ORDER BY p.published_at DESC NULLS LAST, p.id DESC LIMIT $1`, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } items, err := collectPostRows(r, rows) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]any{"items": items}) } func SearchPosts(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) if q == "" { writeJSON(w, PostListResponse{Items: []PostDTO{}}) return } listPostsQuery(w, r, true) } func GetPost(w http.ResponseWriter, r *http.Request) { pool := poolFrom(r) id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { http.Error(w, "bad id", http.StatusBadRequest) return } row := pool.QueryRow(r.Context(), postSelectBase()+` WHERE p.id = $1 AND `+publicPostWhere, id) dto, _, err := scanPostRow(r, row) if err != nil { if err == pgx.ErrNoRows { http.NotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } atts, _ := loadAttachmentsByPostIDs(r.Context(), pool, []uuid.UUID{id}) dto.Attachments = atts[id] if dto.Attachments == nil { dto.Attachments = []AttachmentDTO{} } tags, _ := loadPostTagNames(r.Context(), pool, id, requestLangCode(r)) if tags == nil { tags = []string{} } dto.Tags = tags writeJSON(w, dto) } func PostAttachmentDownload(w http.ResponseWriter, r *http.Request) { pool := poolFrom(r) postID, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { http.Error(w, "bad id", http.StatusBadRequest) return } aid, err := uuid.Parse(chi.URLParam(r, "aid")) if err != nil { http.Error(w, "bad attachment id", http.StatusBadRequest) return } cmd, err := pool.Exec(r.Context(), ` UPDATE posts SET download_count = download_count + 1, updated_at = NOW() WHERE id = $1 AND status = 'published' AND is_public = TRUE AND (published_at IS NULL OR published_at <= NOW())`, postID) if err != nil || cmd.RowsAffected() == 0 { http.NotFound(w, r) return } writeJSON(w, map[string]any{"ok": true}) _ = aid } func listPostsQuery(w http.ResponseWriter, r *http.Request, searchMode bool) { pool := poolFrom(r) limit := postLimitDef(r, 20, 50) cursor := strings.TrimSpace(r.URL.Query().Get("cursor")) catSlug := strings.TrimSpace(r.URL.Query().Get("category")) typ := strings.TrimSpace(r.URL.Query().Get("type")) if typ == "" { typ = "all" } langFilter := strings.TrimSpace(r.URL.Query().Get("language")) base := postSelectBase() + ` WHERE ` + publicPostWhere args := []any{} cond := postTypeFilterSQL(typ, &args) cond += postLanguageFilterSQL(langFilter, &args) if catSlug != "" { args = append(args, catSlug) cond += fmt.Sprintf(` AND c.slug = $%d`, len(args)) } if searchMode { q := strings.TrimSpace(r.URL.Query().Get("q")) if q != "" { pat := "%" + q + "%" args = append(args, pat) n := len(args) cond += fmt.Sprintf(` AND ( p.text_zh ILIKE $%d OR p.text_en ILIKE $%d OR p.text_ja ILIKE $%d OR p.text_ko ILIKE $%d OR p.text_vi ILIKE $%d OR p.text_id ILIKE $%d OR p.text_ms ILIKE $%d OR EXISTS (SELECT 1 FROM post_attachments pa WHERE pa.post_id = p.id AND pa.filename ILIKE $%d) OR EXISTS (SELECT 1 FROM post_tags pt JOIN tags t ON t.id = pt.tag_id WHERE pt.post_id = p.id AND ( t.name ILIKE $%d OR t.name_en ILIKE $%d OR t.name_ja ILIKE $%d OR t.name_ko ILIKE $%d OR t.name_vi ILIKE $%d OR t.name_id ILIKE $%d OR t.name_ms ILIKE $%d)))`, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n) } } order := ` ORDER BY p.published_at DESC NULLS LAST, p.id DESC` if cursor != "" { pubT, cid, err := decodePostCursor(cursor) if err != nil { http.Error(w, "bad cursor", http.StatusBadRequest) return } args = append(args, pubT, cid) n := len(args) cond += fmt.Sprintf(` AND (p.published_at, p.id) < ($%d::timestamptz, $%d::uuid)`, n-1, n) } args = append(args, limit+1) limitArg := len(args) q := base + cond + order + fmt.Sprintf(` LIMIT $%d`, limitArg) rows, err := pool.Query(r.Context(), q, args...) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } items, err := collectPostRows(r, rows) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var nextCursor string if len(items) > limit { last := items[limit] items = items[:limit] if t, err := time.Parse(time.RFC3339, last.PublishedAt); err == nil { uid, _ := uuid.Parse(last.ID) nextCursor = encodePostCursor(t, uid) } } writeJSON(w, PostListResponse{Items: items, NextCursor: nextCursor}) } func collectPostRows(r *http.Request, rows pgx.Rows) ([]PostDTO, error) { defer rows.Close() var ids []uuid.UUID list := make([]PostDTO, 0) for rows.Next() { dto, _, err := scanPostRow(r, rows) if err != nil { return nil, err } uid, _ := uuid.Parse(dto.ID) ids = append(ids, uid) dto.Attachments = []AttachmentDTO{} dto.Tags = []string{} list = append(list, dto) } if err := rows.Err(); err != nil { return nil, err } atts, err := loadAttachmentsByPostIDs(r.Context(), poolFrom(r), ids) if err != nil { return nil, err } lang := requestLangCode(r) tagsByPost, err := loadPostTagNamesByPostIDs(r.Context(), poolFrom(r), ids, lang) if err != nil { return nil, err } for i := range list { uid, _ := uuid.Parse(list[i].ID) if a, ok := atts[uid]; ok { list[i].Attachments = a } if t, ok := tagsByPost[uid]; ok { list[i].Tags = t } else { list[i].Tags = []string{} } } return list, nil }